diff --git a/MVMCoreUI/Atoms/Views/Label.swift b/MVMCoreUI/Atoms/Views/Label.swift index 36843803..5624bb20 100644 --- a/MVMCoreUI/Atoms/Views/Label.swift +++ b/MVMCoreUI/Atoms/Views/Label.swift @@ -9,16 +9,15 @@ import MVMCore -public typealias ActionBlock = () -> Void +public typealias ActionBlock = () -> () @objcMembers open class Label: UILabel, MVMCoreViewProtocol, MVMCoreUIMoleculeViewProtocol, MVMCoreUIViewConstrainingProtocol, MFButtonProtocol { //------------------------------------------------------ // MARK: - Properties //------------------------------------------------------ - + public var makeWholeViewClickable = false - fileprivate var hasAttachmentImage = false /// Set this property if you want updateView to update the font based on this standard and the size passed in. public var standardFontSize: CGFloat = 0.0 @@ -40,14 +39,23 @@ public typealias ActionBlock = () -> Void //------------------------------------------------------ public var clauses: [ActionableClause] = [] { - didSet { isUserInteractionEnabled = !clauses.isEmpty } + didSet { + isUserInteractionEnabled = !clauses.isEmpty + if clauses.count > 1 { + clauses.sort { first, second in + guard let firstLocation = first.range?.location, + let secondLocation = second.range?.location + else { return false } + return firstLocation < secondLocation + } + } + } } /// Used for tappable links in the text. public struct ActionableClause { var range: NSRange? var actionBlock: ActionBlock? -// var text: String? var hash: Int = 0 func performAction() { @@ -66,6 +74,10 @@ public typealias ActionBlock = () -> Void lineBreakMode = .byWordWrapping translatesAutoresizingMaskIntoConstraints = false accessibilityCustomActions = [] + + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(textLinkTapped(_:))) + tapGesture.numberOfTapsRequired = 1 + addGestureRecognizer(tapGesture) } @objc public init() { @@ -113,6 +125,7 @@ public typealias ActionBlock = () -> Void return label } + /// H32 -> Head @objc public static func commonLabelH32(_ scale: Bool) -> Label { let label = Label.label() label.styleH32(scale) @@ -140,12 +153,14 @@ public typealias ActionBlock = () -> Void return label } + /// B20 -> Body @objc public static func commonLabelB20(_ scale: Bool) -> Label { let label = Label.label() label.styleB20(scale) return label } + /// Default @objc open class func label() -> Label { return Label(frame: .zero) } @@ -189,6 +204,10 @@ public typealias ActionBlock = () -> Void } } + if let wholeViewIsClickable = json?.boolForKey("makeWholeViewClickable") { + (label as? Label)?.makeWholeViewClickable = wholeViewIsClickable + } + if let backgroundColorHex = json?.optionalStringForKey(KeyBackgroundColor), !backgroundColorHex.isEmpty { label.backgroundColor = UIColor.mfGet(forHex: backgroundColorHex) } @@ -229,21 +248,27 @@ public typealias ActionBlock = () -> Void case "strikethrough": attributedString.addAttribute(.strikethroughStyle, value: NSUnderlineStyle.thick.rawValue, range: range) attributedString.addAttribute(.baselineOffset, value: 0, range: range) + case "color": if let colorHex = attribute.optionalStringForKey(KeyTextColor), !colorHex.isEmpty { attributedString.removeAttribute(.foregroundColor, range: range) attributedString.addAttribute(.foregroundColor, value: UIColor.mfGet(forHex: colorHex), range: range) } - case "externalLink": + case "image": let fontSize = attribute["size"] as? CGFloat ?? label.font.pointSize - let imageAttachment = Label.getTextAttachmentImage(dimension: fontSize) + let imageName = attribute["name"] as? String ?? "externalLink" + let imageURL = attribute["URL"] as? String + let imageAttachment: NSTextAttachment + + if let url = imageURL, let label = label as? Label { + imageAttachment = Label.getTextAttachmentFrom(url: url, dimension: fontSize, label: label) + } else { + imageAttachment = Label.getTextAttachmentImage(name: imageName, dimension: fontSize) + } let mutableString = NSMutableAttributedString() - mutableString.append(NSAttributedString(string: " ")) mutableString.append(NSAttributedString(attachment: imageAttachment)) - attributedString.insert(mutableString, at: location) - (label as? Label)?.hasAttachmentImage = true case "font": if let fontStyle = attribute.optionalStringForKey("style") { @@ -254,13 +279,13 @@ public typealias ActionBlock = () -> Void } else { let fontSize = attribute["size"] as? CGFloat var font: UIFont? - + if let fontName = attribute.optionalStringForKey("name") { font = MFFonts.mfFont(withName: fontName, size: fontSize ?? label.font.pointSize) } else if let fontSize = fontSize { font = label.font.withSize(fontSize) } - + if let font = font { attributedString.removeAttribute(.font, range: range) attributedString.addAttribute(.font, value: font, range: range) @@ -272,9 +297,8 @@ public typealias ActionBlock = () -> Void actionLabel.addActionAttributes(range: range, string: attributedString) let accessibleAction = actionLabel.customAccessibilityAction(range: range) let actionBlock = actionLabel.createActionBlockFrom(actionMap: json, additionalData: additionalData, delegateObject: delegate) - let actionableClause = ActionableClause(range: range, actionBlock: actionBlock, hash: accessibleAction?.hash ?? -1) - actionLabel.clauses.append(actionableClause) - + actionLabel.clauses.append(ActionableClause(range: range, actionBlock: actionBlock, hash: accessibleAction?.hash ?? -1)) + default: continue } @@ -283,6 +307,10 @@ public typealias ActionBlock = () -> Void } } + //------------------------------------------------------ + // MARK: - Methods + //------------------------------------------------------ + @objc public func styleH1(_ scale: Bool) { MFStyler.styleLabelH1(self, genericScaling: false) setScale(scale) @@ -329,15 +357,28 @@ public typealias ActionBlock = () -> Void if let originalAttributedString = originalAttributedString { let attributedString = NSMutableAttributedString(attributedString: originalAttributedString) attributedString.removeAttribute(.font, range: NSRange(location: 0, length: attributedString.length)) - originalAttributedString.enumerateAttribute(.font, in: NSRange(location: 0, length: originalAttributedString.length), options: [], using: { value, range, stop in - // Loop the original attributed string, resize the fonts. + + // Loop the original attributed string, resize the fonts. + originalAttributedString.enumerateAttribute(.font, in: NSRange(location: 0, length: originalAttributedString.length), options: []) { value, range, stop in + if let fontObj = value as? UIFont, let stylerSize = MFStyler.sizeObjectGeneric(forCurrentDevice: fontObj.pointSize)?.getValueBased(onSize: size) { attributedString.addAttribute(.font, value: fontObj.withSize(stylerSize) as Any, range: range) } - }) + } + + // Loop the original attributed string, resize the image attachments. + originalAttributedString.enumerateAttribute(.attachment, in: NSRange(location: 0, length: originalAttributedString.length), options: []) { value, range, stop in + if let attachment = value as? NSTextAttachment, + let stylerSize = MFStyler.sizeObjectGeneric(forCurrentDevice: attachment.bounds.width)?.getValueBased(onSize: size) { + + let dimension = round(stylerSize) + attachment.bounds = CGRect(x: 0, y: 0, width: dimension, height: dimension) + } + } + attributedText = attributedString - } else if !MVMCoreGetterUtility.fequal(a: Float(standardFontSize), b: 0.0), let sizeObject: MFSizeObject = self.sizeObject ?? MFStyler.sizeObjectGeneric(forCurrentDevice: standardFontSize) { - self.font = self.font.withSize(sizeObject.getValueBased(onSize: size)) + } else if !MVMCoreGetterUtility.fequal(a: Float(standardFontSize), b: 0.0), let sizeObject = sizeObject ?? MFStyler.sizeObjectGeneric(forCurrentDevice: standardFontSize) { + font = font.withSize(sizeObject.getValueBased(onSize: size)) } } @@ -360,10 +401,10 @@ public typealias ActionBlock = () -> Void } /** - Appends an external link image to the end of the attributed string. - Will provide one whitespace to the left of the icon + Appends an external link image to the end of the attributed string. + Will provide one whitespace to the left of the icon */ - public func appendExternalLinkIcon() { + @objc public func appendExternalLinkIcon() { guard let attributedText = attributedText else { return } @@ -372,43 +413,80 @@ public typealias ActionBlock = () -> Void mutableString.append(NSAttributedString(string: " ")) mutableString.append(NSAttributedString(attachment: Label.getTextAttachmentImage(dimension: font.pointSize))) self.attributedText = mutableString - - hasAttachmentImage = true } /** Insert external link icon anywhere within text of Label. - - Parameters: - - index: Location within the associated text to insert an external Link Icon - - Note: You will need to increment your current index by 2 for any changes to the text after inserting an icon. - Each icon insertion adds 2 additional characters to the overall text length. + - Note: Each icon insertion adds 1 additional characters to the overall text length. + Therefore, you MUST insert icons and links in the order they would appear. + - parameter index: Location within the associated text to insert an external Link Icon */ public func insertExternalLinkIcon(at index: Int) { guard let attributedText = attributedText, index <= attributedText.string.count && index >= 0 else { return } let mutableString = NSMutableAttributedString(attributedString: attributedText) - - if index != 0 { - mutableString.insert(NSAttributedString(string: " "), at: index - 1) - } mutableString.insert(NSAttributedString(attachment: Label.getTextAttachmentImage(dimension: font.pointSize)), at: index) - self.attributedText = mutableString - hasAttachmentImage = true + self.attributedText = mutableString } - static func getTextAttachmentImage(dimension: CGFloat) -> NSTextAttachment { + /* + Retrieves an NSTextAttachment for NSAttributedString that is prepped to be inserted with the text. + + - parameter name: The Asset name of the image. DEFAULT: "externalLink" + - parameter dimension: length of the height and width of the image. Will be 80% the passed magnitude. + */ + static func getTextAttachmentImage(name: String = "externalLink", dimension: CGFloat) -> NSTextAttachment { let dimension = round(dimension * 0.8) let imageAttachment = NSTextAttachment() - imageAttachment.image = MVMCoreUIUtility.imageNamed("externalLink") + imageAttachment.image = MVMCoreUIUtility.imageNamed(name) imageAttachment.bounds = CGRect(x: 0, y: 0, width: dimension, height: dimension) return imageAttachment } + + static func getTextAttachmentFrom(url: String, dimension: CGFloat, label: Label) -> NSTextAttachment { + + let dimension = round(dimension * 0.8) + + let imageAttachment = NSTextAttachment() + imageAttachment.bounds = CGRect(x: 0, y: 0, width: dimension, height: dimension) + + DispatchQueue.global(qos: .default).async { + + guard let url = URL(string: url), + let data = try? Data(contentsOf: url) + else { return } + + DispatchQueue.main.sync { + imageAttachment.image = UIImage(data: data) + label.setNeedsDisplay() + } + } + + return imageAttachment + } + + /// Call to detect in the attributedText contains an NSTextAttachment. + func textContainsTextAttachment() -> Bool { + + guard let attributedText = attributedText else { return false } + + var containsAttachment = false + + attributedText.enumerateAttribute(.attachment, in: NSRange(location: 0, length: attributedText.length), options: []) { value, range, stop in + if value is NSTextAttachment { + containsAttachment = true + return + } + } + + return containsAttachment + } } // MARK: - Atomization @@ -419,7 +497,6 @@ extension Label { attributedText = nil textAlignment = .left originalAttributedString = nil - hasAttachmentImage = false styleB2(true) accessibilityCustomActions = [] } @@ -493,9 +570,8 @@ extension Label { Provides an actionable range of text. - Attention: This method expects text to be set first. Otherwise, it will do nothing. - - Parameters: - - range: The range of text to be tapped. - - actionBlock: The code triggered when tapping the range of text. + - parameter range: The range of text to be tapped. + - parameter actionBlock: The code triggered when tapping the range of text. */ @objc public func addTappableLinkAttribute(range: NSRange, actionBlock: @escaping ActionBlock) { @@ -508,11 +584,10 @@ extension Label { Provides an actionable range of text. - Attention: This method expects text to be set first. Otherwise, it will do nothing. - - Parameters: - - range: The range of text to be tapped. - - actionMap: - - delegate: - - additionalData: + - parameter range: The range of text to be tapped. + - parameter actionMap: + - parameter delegate: + - parameter additionalData: */ @objc public func addTappableLinkAttribute(range: NSRange, actionMap: [AnyHashable: Any]?, additionalData: [AnyHashable: Any]?, delegateObject: DelegateObject?) { @@ -525,6 +600,8 @@ extension Label { @objc private func textLinkTapped(_ gesture: UITapGestureRecognizer) { for clause in clauses { + + // This determines if we tapped on the desired range of text. if let range = clause.range, gesture.didTapAttributedTextInLabel(self, inRange: range) { clause.performAction() return @@ -538,29 +615,23 @@ extension UITapGestureRecognizer { func didTapAttributedTextInLabel(_ label: Label, inRange targetRange: NSRange) -> Bool { + // There would only ever be one clause to act on. if label.makeWholeViewClickable { return true } - guard let attributedText = label.attributedText, let font = label.font else { return false } + // Must configure the attributed string to translate what would appear on screen to accurately analyze. + guard let attributedText = label.attributedText else { return false } - var fontToAnalyze: UIFont? = font + let paragraph = NSMutableParagraphStyle() + paragraph.alignment = label.textAlignment - // Necessary where label text is not consistent in font. - for attr in attributedText.attributes(at: targetRange.location, effectiveRange: nil) { - if attr.key == NSAttributedString.Key.font { - fontToAnalyze = attr.value as? UIFont - } - } - - let range = label.hasAttachmentImage ? NSRange(location: 0, length: attributedText.length) : targetRange - - let mutableAttribString = NSMutableAttributedString(attributedString: attributedText) - mutableAttribString.addAttributes([NSAttributedString.Key.font: fontToAnalyze as Any], range: range) + let stagedAttributedString = NSMutableAttributedString(attributedString: attributedText) + stagedAttributedString.addAttributes([NSAttributedString.Key.paragraphStyle: paragraph], range: NSRange(location: 0, length: attributedText.string.count)) + let textStorage = NSTextStorage(attributedString: stagedAttributedString) let layoutManager = NSLayoutManager() let textContainer = NSTextContainer(size: .zero) - let textStorage = NSTextStorage(attributedString: mutableAttribString) layoutManager.addTextContainer(textContainer) textStorage.addLayoutManager(layoutManager)