Im rendering pdf from html content with help of UIPrintPageRenderer, unfortunately sometimes occurs bug, when one element is divided on to parts on different pages. Is there any way to avoid id?
My code and screenshots below:
UIPrintPageRenderer inheritance to specify pdf details.
public class CustomPrintPageRenderer: UIPrintPageRenderer {
fileprivate let offsetX: CGFloat = 40.0
fileprivate let offsetY: CGFloat = 12.0
fileprivate let kHeaderHeight: CGFloat = 36
fileprivate let kFooterHeight: CGFloat = 36
fileprivate let debugFrame = false
fileprivate var name: String?
fileprivate var policy: String?
init(name: String, policy: String, content: String) {
self.name = name
self.policy = policy
super.init()
// Set the page frame.
setValue(NSValue(cgRect: App.A4Rect), forKey: "paperRect")
// Set the printing area frame.
setValue(NSValue(cgRect: App.A4Rect.insetBy(dx: 0, dy: 0)), forKey: "printableRect")
headerHeight = kHeaderHeight
footerHeight = kFooterHeight
// Create formatter
let formatter = UIMarkupTextPrintFormatter(markupText: content)
formatter.perPageContentInsets = UIEdgeInsets(top: kHeaderHeight + 15, left: 36, bottom: 0, right: 36)
addPrintFormatter(formatter, startingAtPageAt: 0)
}
override public func drawHeaderForPage(at pageIndex: Int, in headerRect: CGRect) {
if let name = name as NSString?, let policy = policy as NSString? {
add(name: name, policy: policy, in: headerRect)
}
addLogo(in: headerRect)
if debugFrame {
let path = UIBezierPath(rect: headerRect)
UIColor.red.set()
path.stroke()
}
}
override public func drawFooterForPage(at pageIndex: Int, in footerRect: CGRect) {
addPageNumber(at: pageIndex, in: footerRect)
if debugFrame {
let path = UIBezierPath(rect: footerRect)
UIColor.red.set()
path.stroke()
}
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////
// MARK: -
extension CustomPrintPageRenderer {
fileprivate func addLogo(in headerRect: CGRect) {
let image = #imageLiteral(resourceName: "mss-logo")
let desiredHeight: CGFloat = 40
let newWidth = (desiredHeight * image.size.width) / image.size.height
let imageSize = CGSize(width: newWidth, height: desiredHeight)
let imageRect = CGRect(
x: headerRect.width - imageSize.width - offsetX,
y: headerRect.height - imageSize.height + offsetY,
width: imageSize.width,
height: imageSize.height)
image.draw(in: imageRect)
}
fileprivate func add(name: NSString, policy: NSString, in headerRect: CGRect) {
let text = "\(policy) - \(name)"
let font = UIFont(name: "HelveticaNeue", size: 12.0)
let textSize = getTextSize(text: text as String, font: font!)
let pointX = offsetX
let pointY = headerRect.height - textSize.height
let attributes = [NSAttributedStringKey.font: font!, NSAttributedStringKey.foregroundColor: Color.lightGray]
text.draw(at: CGPoint(x: pointX, y: pointY), withAttributes: attributes)
}
fileprivate func addPageNumber(at pageIndex: Int, in footerRect: CGRect) {
let footerText: NSString = "Page \(pageIndex + 1) of \(numberOfPages)" as NSString
let font = UIFont(name: "HelveticaNeue", size: 12.0)
let textSize = getTextSize(text: footerText as String, font: font!)
let pointX = footerRect.width - textSize.width - offsetX
let pointY = footerRect.origin.y + footerRect.height - textSize.height - 25
let attributes = [NSAttributedStringKey.font: font!]
footerText.draw(at: CGPoint(x: pointX, y: pointY), withAttributes: attributes)
}
fileprivate func getTextSize(text: String, font: UIFont!, textAttributes: [NSAttributedStringKey: AnyObject]! = nil) -> CGSize {
let testLabel = UILabel(frame: CGRect(x: 0.0, y: 0.0, width: paperRect.width, height: footerHeight))
if let attributes = textAttributes {
testLabel.attributedText = NSAttributedString(string: text, attributes: attributes)
}
else {
testLabel.text = text
testLabel.font = font!
}
testLabel.sizeToFit()
return testLabel.frame.size
}
}
** Alert html **
HTML I use to compose alert and add it to pdf.
<tr>
<td></td>
</tr>
<tr>
<div>
<td class="alert" colspan="2">#TEXT#</td>
</div>
</tr>
<tr>
<td></td>
</tr>
** And pdf drawing **
This is how I draw pdf with CustomPrintPageRenderer.
fileprivate func drawPDFUsingPrintPageRenderer(printPageRenderer: UIPrintPageRenderer) -> NSData! {
let data = NSMutableData()
UIGraphicsBeginPDFContextToData(data, App.A4Rect, nil)
for i in 0 ..< printPageRenderer.numberOfPages {
UIGraphicsBeginPDFPage()
printPageRenderer.drawPage(at: i, in: UIGraphicsGetPDFContextBounds())
}
UIGraphicsEndPDFContext()
return data
}
Will be much appreciated for any help!
#media print {
.pagebreak-before:first-child { display: block; page-break-before: avoid; }
.pagebreak-before { display: block; page-break-before: always; }
button {
page-break-inside: avoid;
}
}
Then you probably use those in a class element. Use page break-before for images, etc.
Related
I have two strings that can contain HTML strings.
HTML string can only contain simple text formatting like bold, italic etc.
I need to combine these in an attributed string and show in a list.
Every list item may have a leading or a trailing item or both at the same time.
I am using a UILabel wrapper to have a wrapping text view. I am using this wrapper to show this attributed string. This wrapper only works correctly if I set preferredMaxLayoutWidth value. So I need to give a width to wrapper. when the text view is the only item on horizontal axis it is working fine because I am giving it a constant width:
In case the list with attributed strings, the text view randomly having extra padding at the top and bottom:
This is how I am generating attributed string from HTML string:
func htmlToAttributedString(
fontName: String = AppFonts.hebooRegular.rawValue,
fontSize: CGFloat = 12.0,
color: UIColor? = nil,
lineHeightMultiple: CGFloat = 1
) -> NSAttributedString? {
guard let data = data(using: .unicode, allowLossyConversion: true) else { return nil }
do {
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineHeightMultiple = lineHeightMultiple
paragraphStyle.alignment = .left
paragraphStyle.lineBreakMode = .byWordWrapping
paragraphStyle.lineSpacing = 0
let attributedString = try NSMutableAttributedString(
data: data,
options: [
.documentType: NSAttributedString.DocumentType.html,
.characterEncoding: String.Encoding.utf8.rawValue
],
documentAttributes: nil
)
attributedString.addAttribute(
NSAttributedString.Key.paragraphStyle,
value: paragraphStyle,
range: NSMakeRange(0, attributedString.length)
)
if let font = UIFont(name: fontName, size: fontSize) {
attributedString.addAttribute(
NSAttributedString.Key.font,
value: font,
range: NSMakeRange(0, attributedString.length)
)
}
if let color {
attributedString.addAttribute(
NSAttributedString.Key.foregroundColor,
value: color,
range: NSMakeRange(0, attributedString.length)
)
}
return attributedString
} catch {
return nil
}
}
And this is my UILabel wrapper:
public struct AttributedLabel: UIViewRepresentable {
public func makeUIView(context: Context) -> UIViewType {
let label = UIViewType()
label.numberOfLines = 0
label.lineBreakMode = .byWordWrapping
let result = items.map { item in
let result = item.text.htmlToAttributedString(
fontName: item.fontName,
fontSize: item.fontSize,
color: item.color,
lineHeightMultiple: lineHeightMultiple
)
guard let result else {
return NSAttributedString(string: "")
}
return result
}.reduce(NSMutableAttributedString(string: "")) { result, text in
if result.length > 0 {
result.append(NSAttributedString(string: " "))
}
result.append(text)
return result
}
let height = result.boundingRect(
with: .init(width: width, height: .infinity),
options: [
.usesFontLeading
],
context: nil
)
debugPrint("Calculated height: \(height)")
onHeightCalculated?(height.height)
label.attributedText = result
label.preferredMaxLayoutWidth = width
label.textAlignment = .left
return label
}
public func updateUIView(_ uiView: UIViewType, context: Context) {
uiView.sizeToFit()
}
public typealias UIViewType = UILabel
public let items: [AttributedItem]
public let width: CGFloat
public let lineHeightMultiple: CGFloat
public let onHeightCalculated: ((CGFloat) -> Void)?
public struct AttributedItem {
public let color: UIColor?
public var fontName: String
public var fontSize: CGFloat
public var text: String
public init(
fontName: String,
fontSize: CGFloat,
text: String,
color: UIColor? = nil
) {
self.fontName = fontName
self.fontSize = fontSize
self.text = text
self.color = color
}
}
public init(
items: [AttributedItem],
width: CGFloat,
lineHeightMultiple: CGFloat = 1,
onHeightCalculated: ((CGFloat) -> Void)? = nil
) {
self.items = items
self.width = width
self.lineHeightMultiple = lineHeightMultiple
self.onHeightCalculated = onHeightCalculated
}
}
I am using boundingRect function to calculate the height and passing it to the parent view to set the text view height correctly. Other vise text view getting random height values between 300 to 1000.
This approach only calculates single line height precisely. For multi line texts, text goes out of the calculated bound:
And this is my item component:
private func ItemView(_ item: AppNotification) -> some View {
HStack(alignment: .top, spacing: 10) {
Left(item)
SingleAxisGeometryReader(
axis: .horizontal,
alignment: .topLeading
) { width in
AttributedLabel(
items: [
.init(
fontName: AppFonts.hebooRegular.rawValue,
fontSize: 16,
text: item.data?.message ?? "",
color: UIColor(hexString: "#222222")
),
.init(
fontName: AppFonts.hebooRegular.rawValue,
fontSize: 16,
text: item.timeAgo ?? "",
color: UIColor(hexString: "#727783")
)
],
width: width,
lineHeightMultiple: 0.8,
onHeightCalculated: { textHeight = $0 }
)
.frame(alignment: .topLeading)
.onTapGesture {
if let deepLink = item.data?.deepLink {
viewStore.send(.openDeepLink(deepLink))
}
}
}
.frame(maxHeight: textHeight)
.background(Color.yellow)
Right(item)
}
.padding(.bottom, 16)
}
Can you see what is the problem? Or do you have an other approach?
I am trying to use the HTML as the content of UILabel but getting a weird issue in iOS 14 that the underline color is getting changed as per the text color.
I am using the following code
ViewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
lblHtml2.htmlText = """
<p><span style="text-decoration:underline">I am facing<span style="color:blue;font-weight:bold"> error </span>in version <span style="color:darkolivegreen;font-weight:bold">of greater than 14 </span> for wrong underline color.</span></p>
"""
}
I am using this extension to format the HTML into an attributed string:
extension UILabel {
var htmlText: String? {
get {
do {
if let attrText = attributedText {
let data = try attrText.data(from: NSRange(location: 0, length: attrText.length),
documentAttributes: [NSAttributedString.DocumentAttributeKey.documentType: NSAttributedString.DocumentType.html])
return String(data: data, encoding: .utf8)
}
} catch {
// Fall through
}
return text
}
set {
guard let html = newValue, !html.isEmpty else {
self.text = nil
return
}
do {
let attributedString = try html.htmlToAttributedString(pointSize: font.pointSize, color: textColor, direction: "rtl")
self.attributedText = attributedString
} catch {
self.text = html
}
}
}
}
extension String {
enum Errors: Error {
case AttributedStringConversionError
}
func htmlToAttributedString(pointSize: CGFloat = UIFont.labelFontSize, color: UIColor = .black, direction: String = "ltr") throws -> NSAttributedString {
let fontedHTML = #"""
<html>
<head>
<meta http-equiv="Content-Type" content="text/html"; charset="UTF-8">
<style>
rt {
display: inline
}
body {
font-size: \#(pointSize)px;
font-family: 'system-ui';
color: \#(fontColor(color: color));
}
</style>
</head>
<body>
<div dir="\#(direction)">\#(self)</div>
</body>
</html>
"""#
let data = Data(fontedHTML.utf8)
guard let attrText = try? NSAttributedString(data: data, options: [.documentType: NSAttributedString.DocumentType.html], documentAttributes: nil) else {
throw Errors.AttributedStringConversionError
}
if !attrText.string.isEmpty && attrText.string.hasSuffix("\n") {
return attrText.attributedSubstring(from: NSRange(location: 0, length: attrText.length - 1))
} else {
return attrText
}
}
private func fontColor(color: UIColor) -> String {
var red: CGFloat = 0
var green: CGFloat = 0
var blue: CGFloat = 0
var alpha: CGFloat = 0
color.getRed(&red, green: &green, blue: &blue, alpha: &alpha)
return "rgba(\(Int(red * 255)), \(Int(green * 255)), \(Int(blue * 255)), \(alpha))"
}
}
correct display in iOS 13 :[Expected behavior][1]
incorrect display in iOS14 : [Current Behavior][2]
https://i.stack.imgur.com/VK6Co.png
[2]: https://i.stack.imgur.com/42Pzz.png
I use this code to load my html file with text in WKWebView:
do {
guard let filePath = Bundle.main.path(forResource: "\(readBookNumber)", ofType: "html")
else {
print ("File reading error")
return
}
var content = try String(contentsOfFile: filePath, encoding: .utf8)
let baseUrl = URL(fileURLWithPath: filePath)
content.changeHtmlStyle(font: "Iowan-Old-Style", fontSize: UserDefaults.standard.integer(forKey: "textSize"), fontColor: textColor)
webView.loadHTMLString(headerString+content, baseURL: baseUrl)
}
catch {
print ("File HTML error")
}
and this code to load the page where the user stopped reading last time:
self.webView.scrollView.contentOffset.x = CGFloat(UserDefaults.standard.integer(forKey: "pageToLoad"))
I use code for loading last page in this method:
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.webView.scrollView.contentOffset.x = CGFloat(UserDefaults.standard.integer(forKey: "pageToLoad"))
}
}
At first I used deadline: .now() + 0.1, but that didn't work. Because the last read page was loaded initially, and after a few seconds I see my text on the first page. I change it to deadline: .now() + 0.5 and the text loads fine from the last page read. Its was 700 pages. But now I want to load another text with 1700 pages. And I have same problem like first time. I can change deadline: .now() + 1.0 and my text will load fine. But I think this is not the best solution. I run it on my iPhone X. But maybe if I run it on iPad mini 2 I should change deadline: .now() + 10.0 because iPad mini 2 not very powerful. How to solve the problem?
Update based on #DPrice code:
If I use this code:
override func viewDidLoad() {
super.viewDidLoad()
webView.addObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress), options: .new, context: nil)
....
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if (keyPath == "estimatedProgress") {
if webView.estimatedProgress == 1.0 {
self.webView.scrollView.contentOffset.x = CGFloat(UserDefaults.standard.integer(forKey: "pageToLoad\(self.readBookNumber)"))
}
}
}
I have same bad result like in my code.
But if I use this code:
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if (keyPath == "estimatedProgress") {
if webView.estimatedProgress == 1.0 {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.webView.scrollView.contentOffset.x = CGFloat(UserDefaults.standard.integer(forKey: "pageToLoad\(self.readBookNumber)"))
}
}
}
}
Everything works fine. And my last page loading fine. But it does not solve the problem in my question.
Here is a modified version of your ViewController class:
import UIKit
import WebKit
class ViewController: UIViewController, UIScrollViewDelegate, WKNavigationDelegate {
#IBOutlet weak var webView: WKWebView!
#IBOutlet weak var pagesLabel: UILabel!
var readBookNumber = 0
let headerString = "<meta name=\"viewport\" content=\"initial-scale=1.0\" />"
var textSize = 3
var contentSize: CGSize = .zero
override func viewDidLoad() {
super.viewDidLoad()
// Web View Delegate
webView.scrollView.delegate = self
webView.navigationDelegate = self
webView.scrollView.isPagingEnabled = true
webView.scrollView.alwaysBounceVertical = false
webView.scrollView.showsHorizontalScrollIndicator = true
webView.scrollView.showsVerticalScrollIndicator = false
webView.scrollView.panGestureRecognizer.isEnabled = false
webView.scrollView.pinchGestureRecognizer?.isEnabled = false
webView.scrollView.bouncesZoom = false
self.webView.isOpaque = false;
self.webView.backgroundColor = .clear
// Load File
do {
guard let filePath = Bundle.main.path(forResource: "0", ofType: "html")
else {
print ("File reading error")
return
}
var content = try String(contentsOfFile: filePath, encoding: .utf8)
let baseUrl = URL(fileURLWithPath: filePath)
content.changeHtmlStyle(font: "Iowan-Old-Style", fontSize: 4, fontColor: "black")
webView.loadHTMLString(headerString+content, baseURL: baseUrl)
// add content size Observer
webView.scrollView.addObserver(self, forKeyPath: #keyPath(UIScrollView.contentSize), options: .new, context: nil)
}
catch {
print ("File HTML error")
}
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if (keyPath == #keyPath(UIScrollView.contentSize)) {
let contentSize = webView.scrollView.contentSize
if contentSize != self.contentSize {
self.contentSize = contentSize
DispatchQueue.main.async {
self.webView.scrollView.contentOffset.x = CGFloat(UserDefaults.standard.integer(forKey: "pageToLoad"))
}
}
}
}
// MARK: - webView Scroll View
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
self.stoppedScrolling()
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if !decelerate {
self.stoppedScrolling()
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
var currentPage = Int((webView.scrollView.contentOffset.x / webView.scrollView.frame.size.width) + 1)
let pageCount = Int(webView.scrollView.contentSize.width / webView.scrollView.frame.size.width)
if currentPage == 0 {
currentPage = 1
} else {
}
if !webView.isHidden {
pagesLabel.text = "\( currentPage ) из \( pageCount )"
} else {
pagesLabel.text = ""
}
}
func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) {
webView.scrollView.pinchGestureRecognizer?.isEnabled = false
}
func stoppedScrolling() {
let pageToLoad = Int((webView.scrollView.contentOffset.x))
UserDefaults.standard.set(pageToLoad, forKey: "pageToLoad")
}
// MARK: - loading webView
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
// Маленькая задержка, которую мне хотелось бы использовать
/*DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.webView.scrollView.contentOffset.x = CGFloat(UserDefaults.standard.integer(forKey: "pageToLoad"))
}*/
// Большая задержка, которую мне приходится использовать
// don't do this here... we'll do the "auto-scroll" inside the change contentSize Observer
//DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
// self.webView.scrollView.contentOffset.x = CGFloat(UserDefaults.standard.integer(forKey: "pageToLoad"))
//}
}
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
}
}
extension String {
mutating func changeHtmlStyle(font: String, fontSize: Int, fontColor: String) {
let style = "<font face='\(font)' size='\(fontSize)' color= '\(fontColor)'>%#"
self = String(format: style, self)
}
}
It uses an Observer to watch the contentSize change in the web view's scroll view.
Note that it is called multiple times - with different values - during the load and layout process, but it may do the job for you.
Also note, though, that you'll need to account for changes in the web view size - for example, if the user rotates the device. So... more to do, but this may get you going.
You can add a property observer and watch the estimated progress of the page load:
override func viewDidLoad() {
super.viewDidLoad()
webView.addObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress), options: .new, context: nil)
....
}
and observe when the page is being loaded:
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if (keyPath == "estimatedProgress") {
if webView.estimatedProgress == 1.0 {
print ("page loaded")
}
}
}
You may be able to predict based on the page number how far into the loading process you need to be before you set your offset.
Instead of observing WKWebView.estimatedProgress you should observe UIScrollView.contentSize because you need to scroll to an available position e.g.:
var positionY: CGFloat = 1000
var contentSize = CGSize(width: 0, height: 0)
override func viewDidLoad() {
super.viewDidLoad()
...
webView?.scrollView.addObserver(self, forKeyPath: #keyPath(UIScrollView.contentSize), options: .new, context: nil)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if (keyPath == #keyPath(UIScrollView.contentSize)) {
if let contentSize = webView?.scrollView.contentSize, contentSize != self.contentSize {
self.contentSize = contentSize
if contentSize.height > positionY {
webView?.scrollView.setContentOffset(CGPoint(x: 0, y: positionY), animated: true)
}
}
}
}
I have a table in a mySQL database with the variable "show" that I need to convert from an Int to a Bool within SwiftUI.
Not being able to directly declare 'false' as a field value within SQL - I need to code SwiftUI to interpret this integer as a boolean value.
The JSON output reads as
[
{
"establishmentId": 2,
"name": "O'Reilly's Pub",
"slogan": "Insert slogan here."
"city" : "Insert city here."
"state" : "Insert state here."
"email": "oreillys#email.com",
"phone" : "Insert phone here."
"zip" : 12345
"latitude" : 12.22222222
"longitude" : -31.111111
"hours" : "Insert hours here."
"show" : 0
}
]
In SwiftUI I have a structure called 'Establishment'
struct Establishment: Codable, Identifiable {
let id = UUID()
let name: String
let slogan: String
let city: String
let state: String
let email: String
let phone: String
let zip: Int
let signatureItem: String
let latitude: CLLocationDegrees
let longitude: CLLocationDegrees
let logo: URL
let image: URL
var show: Bool
}
I receive errors when trying to iterate between the establishments due to the 'show' variable being an integer:
import SwiftUI
import SDWebImageSwiftUI
import MapKit
struct EstablishmentList: View {
#ObservedObject var store = DataStore()
#State var active = false
#State var activeIndex = -1
#State var activeView = CGSize.zero
var body: some View {
ZStack {
Color.black.opacity(Double(self.activeView.height/500))
.edgesIgnoringSafeArea(.all)
.statusBar(hidden: active ? true : false)
.animation(.linear)
ScrollView {
VStack(spacing: 30) {
Text("Nearby Establishments")
//.font(.largeTitle).bold()
.font(.system(.largeTitle))
.fontWeight(.bold)
.alignmentGuide(.leading, computeValue: { _ in -30})
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.top, 20)
//.blur(radius: active ? 20 : 0)
.animation(nil)
ForEach(store.establishments.indices, id: \.self) { index in
GeometryReader { geometry in
EstablishmentView(show: self.$store.establishments[index].show,
establishment: self.store.establishments[index],
active: self.$active,
index: index,
activeIndex: self.$activeIndex,
activeView: self.$activeView
)
.offset(y: self.store.establishments[index].show ? -geometry.frame(in: .global).minY : 0)
//.opacity(self.activeIndex != index && self.active ? 0 : 1)
.scaleEffect(self.activeIndex != index && self.active ? 0.5 : 1)
.offset(x: self.activeIndex != index && self.active ? screen.width : 0)
}
.frame(height: getCardHeight())
.frame(maxWidth: self.active ? 712 : getCardWidth())
}
}
.frame(width: screen.width)
.padding(.bottom, 300)
.animation(.spring(response: 0.5, dampingFraction: 0.6, blendDuration: 0))
}
}
}
}
In the EstablishmentView structure I declare '#Binding var show: Bool' and I think this is where my issue rests
EstablishmentView
struct EstablishmentView: View {
#Binding var show: Bool
var establishment: Establishment
#Binding var active: Bool
var index: Int
#Binding var activeIndex: Int
#Binding var activeView: CGSize
var body: some View {
ZStack(alignment: .top) {
VStack(alignment: .leading, spacing: 30.0) {
Text(establishment.name)
Text("About this establishment")
.font(.title)
.fontWeight(.bold)
Text(establishment.slogan)
.foregroundColor(Color("secondary"))
Text(establishment.signatureItem)
.foregroundColor(Color("secondary"))
}
.padding(30)
.offset(y: show ? 460 : 0)
.frame(maxWidth: show ? .infinity : getCardWidth())
.frame(maxHeight: show ? screen.height : 280, alignment: .top)
.background(Color("background2"))
.clipShape(RoundedRectangle(cornerRadius: 30, style: .continuous))
.shadow(color: Color.black.opacity(0.2), radius: 20, x: 0, y: 20)
.opacity(show ? 1 : 0)
VStack {
HStack(alignment: .top) {
VStack(alignment: .leading, spacing: 8.0) {
Text(establishment.name)
.font(.system(size: 24, weight: .bold))
.lineLimit(3)
.foregroundColor(.white)
.animation(nil)
Text(establishment.email.uppercased())
.foregroundColor(Color.white.opacity(0.7))
.animation(nil)
Text(establishment.state)
.foregroundColor(Color.white).opacity(0.7)
.animation(nil)
}
Spacer()
ZStack {
WebImage(url: establishment.image)
.opacity(show ? 0 : 1)
VStack {
Image(systemName: "xmark")
.font(.system(size: 16, weight: .medium))
.foregroundColor(.white)
}
.frame(width: 36, height: 36)
.background(Color.black)
.clipShape(Circle())
.opacity(show ? 1 : 0)
}
}
Spacer()
WebImage(url: establishment.image)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(maxWidth: 414)
.frame(height: 140, alignment: .top)
}
.padding(show ? 30 : 20)
.padding(.top, show ? 30 : 0)
.frame(height: show ? 460 : 280)
.frame(maxWidth: show ? .infinity : getCardWidth())
.background(Color(#colorLiteral(red: 0.5843137503, green: 0.8235294223, blue: 0.4196078479, alpha: 1)))
.clipShape(RoundedRectangle(cornerRadius: 30, style: .continuous))
.shadow(color: Color(#colorLiteral(red: 0.5843137503, green: 0.8235294223, blue: 0.4196078479, alpha: 1)).opacity(0.3), radius: 20, x: 0, y: 20)
.gesture(
show ?
DragGesture()
.onChanged { value in
guard !self.show else { return }
guard value.translation.height > 0 else { return }
guard value.translation.height < 300 else { return }
self.activeView = value.translation
}
.onEnded { value in
if self.activeView.height > 50 {
self.show = false
self.active = false
self.activeIndex = -1
}
self.activeView = .zero
}
: nil
)
.onTapGesture {
self.show.toggle()
self.active.toggle()
if self.show {
self.activeIndex = self.index
} else {
self.activeIndex = -1
}
}
if show {
EstablishmentDetail(establishment: establishment, show: $show, active: $active, activeIndex: $activeIndex)
.background(Color("background1"))
.animation(.linear(duration: 0))
}
}
.gesture(
show ?
DragGesture()
.onChanged { value in
guard value.translation.height > 0 else { return }
guard value.translation.height < 300 else { return }
self.activeView = value.translation
}
.onEnded { value in
if self.activeView.height > 50 {
self.show = false
self.active = false
self.activeIndex = -1
}
self.activeView = .zero
}
: nil
)
.frame(height: show ? screen.height : 280)
.edgesIgnoringSafeArea(.all)
.animation(.spring(response: 0.5, dampingFraction: 0.6, blendDuration: 0))
.scaleEffect(1 - self.activeView.height / 1000)
.rotation3DEffect(Angle(degrees: Double(self.activeView.height / -10)), axis: (x: 10, y: -10, z: 0))
.hueRotation(Angle(degrees: Double(self.activeView.height)))
}
}
(I have been working with hardcoded values which is why I never ran into issues when I declared 'var show = false' in my original Establishment struct.)
In an APIManager class I call on my API
import SwiftUI
class APIManager {
func getEstablishments(completion: #escaping ([Establishment]) -> ()) {
guard let url = URL(string: "api address here") else { return }
URLSession.shared.dataTask(with: url) { (data, _, _) in
guard let data = data else { return }
let establishments = try! JSONDecoder().decode([Establishment].self, from: data)
DispatchQueue.main.async {
completion(establishments)
}
}
.resume()
}
and in a DataStore class I initialize a function to utilize the APIManager
import SwiftUI
import Combine
class DataStore: ObservableObject {
#Published var establishments: [Establishment] = []
init() {
getEstablishments()
}
func getEstablishments() {
APIManager().getEstablishments { (establishments) in
self.establishments = establishments
}
}
Can anybody recommend a method of converting the Int to Bool datatype within SwiftUI - that would eliminate my errors. I hope i've provided enough of my code to be clear but let me know if there is more I can provide for clarity's sake.
Edit: Replaced images of code with actual text
Edit: Results from Chris's answer
extension Establishment: Decodable {
private struct JSONSettings: Decodable {
var id = UUID()
var name, slogan, city, state, email, phone, signatureItem: String
var latitude, longitude: Double
var logo, image: String
var zip, show: Int
}
private enum CodingKeys: String, CodingKey {
case establishmentList // Top level
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let settings = try container.decode(JSONSettings.self, forKey: .establishmentList)
id = settings.id
name = settings.name
slogan = settings.slogan
city = settings.city
state = settings.state
email = settings.email
phone = settings.phone
zip = settings.zip
signatureItem = settings.signatureItem
latitude = settings.latitude
longitude = settings.longitude
show = settings.show == 1 ? true : false
}
}
You can use a custom initialiser for the struct that converts a String or Int to Bool.
struct Establishment {
let establishmentID: Int
let name, email: String
let show: Bool
}
extension Establishment: Decodable {
private struct JSONSettings: Decodable {
var establishmentId: String
var name: String
var email: String
var show: Int
}
private enum CodingKeys: String, CodingKey {
case establishmentList // Top level
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let settings = try container.decode(JSONSettings.self, forKey: .establishmentList)
establishmentId = settings.establishmentId
name = settings.name
email = settings.email
show = settings.show == 1 ? true : false
}
}
All credit to this answer.
I created a UILabel and synced its font size with a seekbar and loaded my html formatted text onto the UILabel. Unfortunately, the format gets removed if I change the font size of my UILabel using seekbar.
I used this approach, from garie, that was answered from another thread
private func getHtmlLabel(text: String) -> UILabel {
let label = UILabel()
label.numberOfLines = 0
label.lineBreakMode = .byWordWrapping
label.attributedString = stringFromHtml(string: text)
return label
}
private func stringFromHtml(string: String) -> NSAttributedString? {
do {
let data = string.data(using: String.Encoding.utf8, allowLossyConversion: true)
if let d = data {
let str = try NSAttributedString(data: d,
options: [NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType],
documentAttributes: nil)
return str
}
} catch {
}
return nil
}
Link: Swift: Display HTML data in a label or textView
Is there any approach I could use to retain the my html formatted text?
EDIT #1:
This is how I resize my UILabel.
#IBOutlet weak var textLabel: UILabel!
#IBAction func resizeText(_ sender: UISlider) {
textLabel.font = UIFont.systemFont(ofSize: CGFloat(sender.value) * 20.0)
}
The HTML data I'm passing onto my UILabel:
<html><body><b>hello world</b></body></html>
This is the formatted HTML data on UILabel:
After resizing the UILabel, it loses its format:
EDIT #2:
Tried Milan's answer but encountered an error. Unresolved identifier in NSAttributedStringKey
Here's my code:
import UIKit
class GalleryViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
textLabel.attributedText = stringFromHtml()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
#IBOutlet weak var textLabel: UILabel!
#IBAction func resizeText(_ sender: UISlider) {
if let attributedText = textLabel.attributedText {
let newAttributedText = NSMutableAttributedString(attributedString: attributedText)
newAttributedText.setFontSize(newSize: CGFloat(sender.value) * 20.0)
textLabel.attributedText = newAttributedText
}
}
private func stringFromHtml() -> NSAttributedString? {
do {
let string = "<html><body><b>hello world</b></body></html>"
let data = string.data(using: String.Encoding.utf8, allowLossyConversion: true)
if let d = data {
let str = try NSAttributedString(data: d,
options: [NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType],
documentAttributes: nil)
return str
}
} catch {
}
return nil
}
}
extension NSMutableAttributedString {
func setFontSize(newSize: CGFloat) {
beginEditing()
self.enumerateAttribute(NSAttributedStringKey.font, in: NSRange(location: 0, length: self.length)) { (value, range, stop) in
if let f = value as? UIFont {
let newFont = f.withSize(newSize)
removeAttribute(NSAttributedStringKey.font, range: range)
addAttribute(NSAttributedStringKey.font, value: newFont, range: range)
}
}
endEditing()
}
}
Setting the font directly messes up your attributed text. You will have to set font size on the attributedText:
#IBAction func resizeText(_ sender: UISlider) {
if let attributedText = textLabel.attributedText {
let newAttributedText = NSMutableAttributedString(attributedString: attributedText)
newAttributedText.setFontSize(newSize: CGFloat(sender.value) * 20.0)
textLabel.attributedText = newAttributedText
}
}
For this to work you will need following extension on NSMutableAttributedString:
extension NSMutableAttributedString {
func setFontSize(newSize: CGFloat) {
beginEditing()
self.enumerateAttribute(NSAttributedStringKey.font, in: NSRange(location: 0, length: self.length)) { (value, range, stop) in
if let f = value as? UIFont {
let newFont = f.withSize(newSize)
removeAttribute(NSAttributedStringKey.font, range: range)
addAttribute(NSAttributedStringKey.font, value: newFont, range: range)
}
}
endEditing()
}
}
Try using UserDefaults to save the the NSAttributedString returning from second function:
1- Hold the attributed text in a variable
let attributedText = stringFromHtml(string: String)
2- Save it in UserDefaults
UserDefaults.standard.set(attributedText, forKey: "attributedText")
And for retrieving it you may use this format to safely unwrap the value:
if let text = UserDefaults.standard.object(forKey: "attributedText") as? NSAttributedString {
label.attributedString = text
}