diff --git a/MVMCoreUI/Atoms/Views/Label.swift b/MVMCoreUI/Atoms/Views/Label.swift index 06ddbed5..4346dfa0 100644 --- a/MVMCoreUI/Atoms/Views/Label.swift +++ b/MVMCoreUI/Atoms/Views/Label.swift @@ -9,14 +9,14 @@ import MVMCore -public typealias ActionBlock = () -> Void +public typealias ActionBlock = () -> () @objcMembers open class Label: UILabel, MVMCoreViewProtocol, MVMCoreUIMoleculeViewProtocol, MVMCoreUIViewConstrainingProtocol, MFButtonProtocol { //------------------------------------------------------ // MARK: - General Properties //------------------------------------------------------ - + public var makeWholeViewClickable = false /// Set this property if you want updateView to update the font based on this standard and the size passed in. @@ -39,7 +39,17 @@ 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. @@ -113,6 +123,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 +151,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 +202,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,11 +246,28 @@ 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 "image": + let fontSize = attribute["size"] as? CGFloat ?? label.font.pointSize + 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(attachment: imageAttachment)) + attributedString.insert(mutableString, at: location) + case "font": if let fontStyle = attribute.optionalStringForKey("style") { let styles = MFStyler.styleGetAttributedString("0", withStyle: fontStyle) @@ -243,13 +277,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) @@ -260,9 +294,9 @@ public typealias ActionBlock = () -> Void actionLabel.addActionAttributes(range: range, string: attributedString) actionLabel.clauses.append(ActionableClause(range: range, - actionBlock: actionLabel.createActionBlockFrom(actionMap: json, - additionalData: additionalData, - delegateObject: delegate))) + actionBlock: actionLabel.createActionBlockFrom(actionMap: json, + additionalData: additionalData, + delegateObject: delegate))) default: continue } @@ -271,8 +305,6 @@ public typealias ActionBlock = () -> Void } } - - //------------------------------------------------------ // MARK: - Methods //------------------------------------------------------ @@ -323,15 +355,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)) } } @@ -353,23 +398,92 @@ 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 + */ + @objc public func appendExternalLinkIcon() { + + guard let attributedText = attributedText else { return } + + let mutableString = NSMutableAttributedString(attributedString: attributedText) + + mutableString.append(NSAttributedString(string: " ")) + mutableString.append(NSAttributedString(attachment: Label.getTextAttachmentImage(dimension: font.pointSize))) + self.attributedText = mutableString + } - ///Appends an external link image to the end of the attributed string. - public func addExternalLinkIcon() { + /** + Insert external link icon anywhere within text of Label. + + - 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) { - let size = round(font.pointSize * 0.8) + guard let attributedText = attributedText, index <= attributedText.string.count && index >= 0 else { return } - guard let attributedText = self.attributedText else { return } + let mutableString = NSMutableAttributedString(attributedString: attributedText) + mutableString.insert(NSAttributedString(attachment: Label.getTextAttachmentImage(dimension: font.pointSize)), at: index) - let fullString = NSMutableAttributedString(attributedString: attributedText) + self.attributedText = mutableString + } + + /* + 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.bounds = CGRect(x: 0, y: 0, width: size, height: size) + imageAttachment.image = MVMCoreUIUtility.imageNamed(name) + imageAttachment.bounds = CGRect(x: 0, y: 0, width: dimension, height: dimension) - fullString.append(NSAttributedString(string: " ")) - fullString.append(NSAttributedString(attachment: imageAttachment)) - self.attributedText = fullString + 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 } } @@ -452,9 +566,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) { @@ -466,11 +579,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?) { @@ -484,6 +596,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 @@ -497,15 +611,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 } + // Must configure the attributed string to translate what would appear on screen to accurately analyze. guard let attributedText = label.attributedText else { return false } + let paragraph = NSMutableParagraphStyle() + paragraph.alignment = label.textAlignment + + 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: attributedText) layoutManager.addTextContainer(textContainer) textStorage.addLayoutManager(layoutManager) @@ -515,8 +637,8 @@ extension UITapGestureRecognizer { textContainer.maximumNumberOfLines = label.numberOfLines textContainer.size = label.bounds.size - let indexOfCharacter = layoutManager.characterIndex(for: location(in: label), in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil) - - return NSLocationInRange(indexOfCharacter, targetRange) + let indexOfGlyph = layoutManager.glyphIndex(for: location(in: label), in: textContainer) + + return NSLocationInRange(indexOfGlyph, targetRange) } }