SwiftUI: Attributed string from HTML that wraps - html

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?

Related

How to change font size of UTF16(Gujarati language) attributed string in function and use it to convert json nested data in swiftUI?

i'm currently showing json data in SwiftUI list using function:
#available(iOS 15, *)
func attributedString(from str: String) -> AttributedString {
if let theData = str.data(using: .utf16 ) {
do {
let theString = try NSAttributedString( data: theData, options: [.documentType: NSAttributedString.DocumentType.html], documentAttributes: nil )
return AttributedString(theString)
} catch { print("\(error)") }
}
return AttributedString(str)
}
and putting it in
import SwiftUI
struct lvl4: View {
#State var books: [Buk] = []
#State var selection: Buk?
//ios 14 must to get the syntex right..
#available(iOS 14, *)
var body: some View {
NavigationView {
List(books) { book in
ForEach(book.bookContent) { bookContent in
Section(header: Text(bookContent.title).font(.largeTitle) .fontWeight(.heavy)) {
OutlineGroup(bookContent.child, children: \.child) { item in
if #available(iOS 15, *) {
Text(attributedString(from: item.title))
}
}
}
}
// a file with valid json data for testing
func loadData() {
do {
if let url = Bundle.main.url(forResource: "સાગર મંથન_new", withExtension: "json") {
let data = try Data(contentsOf: url)
books = try JSONDecoder().decode([Buk].self, from: data)
}
} catch {
print("error: \(error)")
}
}
}
struct Buk: Identifiable, Codable {
let id = UUID()
var bookTitle: String = ""
var isLive: Bool = false
var userCanCopy: Bool = false
var bookContent: [BookContent] = []
enum CodingKeys: String, CodingKey {
case bookTitle = "book_title"
case isLive = "is_live"
case userCanCopy = "user_can_copy"
case bookContent = "book_content"
}
}
struct BookContent: Identifiable, Codable {
let id = UUID()
var title, type: String
var child: [Child]
}
struct Child: Identifiable, Codable {
let id = UUID()
var title, type: String
var child: [Child]?
}
i need to customise the font and size of the attributed string before it is returned as below which are very small by default when it is converted from utf16.. The code is for swiftUI and the function i created is for using it in parsing json file
very small text..
I thought you would make an effort and find the answer yourself,
but I can see you could not. Try this:
func attributedString(from str: String, font: Font) -> AttributedString {
if let theData = str.data(using: .utf16) {
do {
let theString = try NSAttributedString(data: theData, options: [.documentType: NSAttributedString.DocumentType.html], documentAttributes: nil)
var attaString = AttributedString(theString)
attaString.font = font // <-- here
return attaString
} catch {
print("\(error)")
}
}
return AttributedString(str)
}
and call it like this:
Text(attributedString(from: item.title, font: Font.system(size: 30)))

HTML text with underline has unexpected appearance with attributed string in version iOS 14

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

UIPrintPageRenderer breaks content on different pages and skips part of page

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.

How do I convert an Int value (from a mySQL database) to a Bool value in SwiftUI?

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.

Swift3 How to set properties created in an extension

I have a function in my UIViewControllers to setup a UINavigationBar which is repeated in many functions. I want to create the navigation bar in an extension but I want to set the title text and a cart label in each function. How can I do this? I think the answer is to use protocols but I'm not sure how.
Here is my extension
extension UIViewController {
func shoppingBagButtonTouched(button: UIButton) {
-----
}
func closeView() {
dismiss(animated: true, completion: nil)
}
func setupNavigationHeader(showCart: Bool? = true) {
let navigationBar: UINavigationBar = {
let navBar = UINavigationBar(frame: CGRect(0, 0, self.view.frame.size.width, Constants.HEADER_HEIGHT))
return navBar
}()
let navigationItem = UINavigationItem()
self.automaticallyAdjustsScrollViewInsets = false
UINavigationBar.appearance().barTintColor = .red
UINavigationBar.appearance().tintColor = .white
let titleLabel: UILabel = {
let label = UILabel()
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
label.backgroundColor = .clear
label.layer.masksToBounds = true
label.minimumScaleFactor = 10/UIFont.labelFontSize
label.adjustsFontSizeToFitWidth = true
label.numberOfLines = 1
label.text = "not set"
label.textColor = .white
return label
}()
let fixedSpace = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil)
let menuBtn = UIBarButtonItem(image: UIImage(named: "closeNav"), style: .plain, target: self, action: #selector(self.closeView))
navigationItem.leftBarButtonItems = [fixedSpace, menuBtn]
navigationBar.items = [navigationItem]
if let showCart = showCart {
let cartCountLabel: UILabel = {
let label = UILabel(frame: CGRect(x: 0, y: -0, width: 20, height: 20))
label.textAlignment = .center
label.layer.cornerRadius = label.bounds.size.height / 2
label.translatesAutoresizingMaskIntoConstraints = false
label.backgroundColor = .clear
label.layer.masksToBounds = true
label.textColor = .white
label.minimumScaleFactor = 10/UIFont.labelFontSize
label.adjustsFontSizeToFitWidth = true
return label
}()
let shoppingBagButton: UIButton = {
let button = UIButton(frame: CGRect(x: 0, y: 0, width: 22, height: 22))
button.setBackgroundImage(UIImage(named: "shopping_bag"), for: .normal)
return button
}()
let rightBarButtonItem = UIBarButtonItem(customView: shoppingBagButton)
navigationItem.setRightBarButtonItems([rightBarButtonItem], animated: true)
shoppingBagButton.addTarget(self, action: #selector(shoppingBagButtonTouched(button:)), for: .touchUpInside)
shoppingBagButton.addSubview(cartCountLabel)
cartCountLabel.anchorCenterXToSuperview()
cartCountLabel.anchorCenterYToSuperview(constant: 2)
}
navigationBar.addSubview(titleLabel)
view.addSubview(navigationBar)
titleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
titleLabel.bottomAnchor.constraint(equalTo: navigationBar.bottomAnchor, constant: -10).isActive = true
titleLabel.heightAnchor.constraint(equalToConstant: 20.0).isActive = true
titleLabel.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width - 90).isActive = true
}
}
In each controller I have code like this to set the title label and cart label but it doesn't work when I create the nav bar in an extension.
var titleLabelText: String = "title not set"
var cartCount: String? {
didSet {
cartCountLabel.text = cartCount
}
}
func getCartCount() {
ServerUtility.getCartCountApi { (cartCount) in
if let count = cartCount as Int? {
if count > 0 {
self.cartCount = "\(count)"
}
} else {
self.cartCount = "0"
}
}
}
You are probably best off creating a base class extending UIViewController something like this:
class BaseViewController: UIViewController {
let cartCountLabel: UILabel = {
let label = UILabel(frame: CGRect(x: 0, y: -0, width: 20, height: 20))
label.textAlignment = .center
label.layer.cornerRadius = label.bounds.size.height / 2
label.translatesAutoresizingMaskIntoConstraints = false
label.backgroundColor = .clear
label.layer.masksToBounds = true
label.textColor = .white
label.minimumScaleFactor = 10/UIFont.labelFontSize
label.adjustsFontSizeToFitWidth = true
return label
}()
func shoppingBagButtonTouched(button: UIButton) {
-----
}
func closeView() {
dismiss(animated: true, completion: nil)
}
func setupNavigationHeader(showCart: Bool? = true) {
let navigationBar: UINavigationBar = {
let navBar = UINavigationBar(frame: CGRect(0, 0, self.view.frame.size.width, Constants.HEADER_HEIGHT))
return navBar
}()
let navigationItem = UINavigationItem()
self.automaticallyAdjustsScrollViewInsets = false
UINavigationBar.appearance().barTintColor = .red
UINavigationBar.appearance().tintColor = .white
let titleLabel: UILabel = {
let label = UILabel()
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
label.backgroundColor = .clear
label.layer.masksToBounds = true
label.minimumScaleFactor = 10/UIFont.labelFontSize
label.adjustsFontSizeToFitWidth = true
label.numberOfLines = 1
label.text = "not set"
label.textColor = .white
return label
}()
let fixedSpace = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil)
let menuBtn = UIBarButtonItem(image: UIImage(named: "closeNav"), style: .plain, target: self, action: #selector(self.closeView))
navigationItem.leftBarButtonItems = [fixedSpace, menuBtn]
navigationBar.items = [navigationItem]
if let showCart = showCart {
let shoppingBagButton: UIButton = {
let button = UIButton(frame: CGRect(x: 0, y: 0, width: 22, height: 22))
button.setBackgroundImage(UIImage(named: "shopping_bag"), for: .normal)
return button
}()
let rightBarButtonItem = UIBarButtonItem(customView: shoppingBagButton)
navigationItem.setRightBarButtonItems([rightBarButtonItem], animated: true)
shoppingBagButton.addTarget(self, action: #selector(shoppingBagButtonTouched(button:)), for: .touchUpInside)
shoppingBagButton.addSubview(cartCountLabel)
cartCountLabel.anchorCenterXToSuperview()
cartCountLabel.anchorCenterYToSuperview(constant: 2)
}
navigationBar.addSubview(titleLabel)
view.addSubview(navigationBar)
titleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
titleLabel.bottomAnchor.constraint(equalTo: navigationBar.bottomAnchor, constant: -10).isActive = true
titleLabel.heightAnchor.constraint(equalToConstant: 20.0).isActive = true
titleLabel.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width - 90).isActive = true
}
}
Then you can derive all your view controller instances from the base class like this:
class MyViewController:BaseViewController {
var titleLabelText: String = "title not set"
var cartCount: String? {
didSet {
cartCountLabel.text = cartCount
}
}
func getCartCount() {
ServerUtility.getCartCountApi { (cartCount) in
if let count = cartCount as Int? {
if count > 0 {
self.cartCount = "\(count)"
}
} else {
self.cartCount = "0"
}
}
}
}
I have not verified the above code by running it and so it might need a bit of tweaking :)