From 81ccaa5bbd4a9cefefd59f03e69c83c654095b0f Mon Sep 17 00:00:00 2001 From: Kevin G Christiano Date: Fri, 26 Jul 2019 13:39:28 -0400 Subject: [PATCH] Restructuring the tappable text feature. Must account of the offset incurred by adding an extra space and image attachment to the overall length of the label. Included a sorting feature to ensure clauses array is always in ascending order by location. --- MVMCoreUI/Atoms/Views/Label.swift | 97 +++++++++++++++++++++---------- 1 file changed, 67 insertions(+), 30 deletions(-) diff --git a/MVMCoreUI/Atoms/Views/Label.swift b/MVMCoreUI/Atoms/Views/Label.swift index fd9349fe..5792f1c7 100644 --- a/MVMCoreUI/Atoms/Views/Label.swift +++ b/MVMCoreUI/Atoms/Views/Label.swift @@ -18,7 +18,9 @@ public typealias ActionBlock = () -> Void //------------------------------------------------------ public var makeWholeViewClickable = false - fileprivate var hasAttachmentImage = false + + /// Indication that attributed text attachment will affect range location of attributed text. + fileprivate var hasAttachmentImageInsideText = 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,7 +42,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. @@ -212,6 +224,8 @@ public typealias ActionBlock = () -> Void let range = NSRange(location: location, length: length) + print(attributeType) + switch attributeType { case "underline": attributedString.addAttribute(.underlineStyle, value: NSUnderlineStyle.single.rawValue, range: range) @@ -233,7 +247,10 @@ public typealias ActionBlock = () -> Void mutableString.append(NSAttributedString(attachment: imageAttachment)) attributedString.insert(mutableString, at: location) - (label as? Label)?.hasAttachmentImage = true + + if location < attributedString.length { + (label as? Label)?.hasAttachmentImageInsideText = true + } case "font": if let fontStyle = attribute.optionalStringForKey("style") { @@ -332,19 +349,20 @@ public typealias ActionBlock = () -> Void 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 value is NSTextAttachment, 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 = 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)) } } @@ -379,8 +397,6 @@ public typealias ActionBlock = () -> Void mutableString.append(NSAttributedString(string: " ")) mutableString.append(NSAttributedString(attachment: Label.getTextAttachmentImage(dimension: font.pointSize))) self.attributedText = mutableString - - hasAttachmentImage = true } /** @@ -403,7 +419,9 @@ public typealias ActionBlock = () -> Void mutableString.insert(NSAttributedString(attachment: Label.getTextAttachmentImage(dimension: font.pointSize)), at: index + 1) self.attributedText = mutableString - hasAttachmentImage = true + if index < attributedText.length { + hasAttachmentImageInsideText = true + } } static func getTextAttachmentImage(dimension: CGFloat) -> NSTextAttachment { @@ -425,7 +443,7 @@ extension Label { text = nil attributedText = nil originalAttributedString = nil - hasAttachmentImage = false + hasAttachmentImageInsideText = false styleB2(true) } @@ -528,8 +546,36 @@ extension Label { @objc private func textLinkTapped(_ gesture: UITapGestureRecognizer) { + var offset = 0 + var attachmentLocations: [Int] = [] + var attachmentIndex = 0 + + // Used to identify all images attached in the label. + if hasAttachmentImageInsideText { + attributedText?.enumerateAttribute(.attachment, in: NSRange(location: 0, length: attributedText!.length), options: []) { value, range, stop in + guard value is NSTextAttachment else { return } + + attachmentLocations.append(range.location) + + if range.location == 0 { + offset = 1 + } + } + } + for clause in clauses { - if let range = clause.range, gesture.didTapAttributedTextInLabel(self, inRange: range) { + guard let range = clause.range else { return } + + // Must increment an offset becuase every image added to text interior increases text length by 1 or 2 depending on location. + if hasAttachmentImageInsideText { + if range.location > attachmentLocations[attachmentIndex] { + offset += 2 + attachmentIndex += 1 + } + } + + // This determines if we tapped on the desired range of text. + if gesture.didTapAttributedTextInLabel(self, inRange: range, offset: offset) { clause.performAction() return } @@ -540,31 +586,17 @@ extension Label { // MARK: - extension UITapGestureRecognizer { - func didTapAttributedTextInLabel(_ label: Label, inRange targetRange: NSRange) -> Bool { + func didTapAttributedTextInLabel(_ label: Label, inRange targetRange: NSRange, offset: Int) -> Bool { if label.makeWholeViewClickable { return true } - guard let attributedText = label.attributedText, let font = label.font else { return false } - - var fontToAnalyze: UIFont? = font - - // 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) - + guard let attributedText = label.attributedText else { return false } + + let textStorage = NSTextStorage(attributedString: attributedText) let layoutManager = NSLayoutManager() let textContainer = NSTextContainer(size: .zero) - let textStorage = NSTextStorage(attributedString: mutableAttribString) layoutManager.addTextContainer(textContainer) textStorage.addLayoutManager(layoutManager) @@ -576,6 +608,11 @@ extension UITapGestureRecognizer { let indexOfGlyph = layoutManager.glyphIndex(for: location(in: label), in: textContainer) + if label.hasAttachmentImageInsideText { + let location = targetRange.location + offset + return NSLocationInRange(indexOfGlyph, NSRange(location: location, length: targetRange.length)) + } + return NSLocationInRange(indexOfGlyph, targetRange) } }