diff --git a/MVMCoreUI/Atoms/Views/Label.swift b/MVMCoreUI/Atoms/Views/Label.swift index b203e488..7acb4691 100644 --- a/MVMCoreUI/Atoms/Views/Label.swift +++ b/MVMCoreUI/Atoms/Views/Label.swift @@ -20,6 +20,9 @@ public typealias ActionBlock = () -> Void var actionText: String? var frontText: String? + var clauses = [ActionableClause]() + + // This is here for LabelWithInternalButton var makeWholeViewClickable = false // Set this property if you want updateView to update the font based on this standard and the size passed in. @@ -38,6 +41,26 @@ public typealias ActionBlock = () -> Void return !text.isEmpty || !attributedText.string.isEmpty } + struct ActionableClause { + var location: Int? + var length: Int? + var actionText: String? + var words: [String]? { + return actionText?.components(separatedBy: " ") + } + var actionBlock: ActionBlock? + + var range: NSRange { + return NSRange(location: location ?? 0, length: length ?? 0) + } + + func performAction() { + if let action = actionBlock { + action() + } + } + } + //------------------------------------------------------ // MARK: - Initialization //------------------------------------------------------ @@ -148,7 +171,7 @@ public typealias ActionBlock = () -> Void @objc public static func setUILabel(_ label: UILabel?, withJSON json: [AnyHashable: Any]?, delegate: DelegateObject?, additionalData: [AnyHashable: Any]?) { - guard let label = label else { return } + guard let label = label as? Label else { return } label.text = json?.optionalStringForKey(KeyText) @@ -176,7 +199,6 @@ public typealias ActionBlock = () -> Void let attributedString = NSMutableAttributedString(string: labelText, attributes: [NSAttributedString.Key.font: label.font as UIFont, NSAttributedString.Key.foregroundColor: label.textColor as UIColor]) for case let attribute as [String: Any] in attributes { - guard let attributeType = attribute.optionalStringForKey(KeyType), let location = attribute["location"] as? Int, let length = attribute["length"] as? Int @@ -210,6 +232,29 @@ public typealias ActionBlock = () -> Void attributedString.removeAttribute(.font, range: range) attributedString.addAttribute(.font, value: font, range: range) } + case "actions": + let actions = attribute.arrayForKey("actions") + let text = json?.optionalStringForKey(KeyText) + + for case let action as [String: Any] in actions { + guard let locationx = action["location"] as? Int, + let lengthx = action["length"] as? Int + else { continue } + + let subStringRange = Range(NSRange(location: locationx, length: lengthx), in: text!) + let actionText = text![subStringRange!] + + label.clauses.append(ActionableClause(location: locationx, length: lengthx, + actionText: String(actionText), + actionBlock: { MVMCoreActionHandler.shared()?.handleAction(with: attribute, additionalData: additionalData, delegateObject: delegate) })) + if !label.isUserInteractionEnabled { + label.isUserInteractionEnabled = true + let tapGesture = UITapGestureRecognizer(target: label, action: #selector(textLinkTapped(_:))) + tapGesture.numberOfTapsRequired = 1 + label.addGestureRecognizer(tapGesture) + } + } + default: continue } @@ -308,22 +353,29 @@ public typealias ActionBlock = () -> Void } public func needsToBeConstrained() -> Bool { - return true; + return true } } // MARK: - UIControl Override extension Label { - override open func touchesEnded(_ touches: Set, with event: UIEvent?) { + @objc func textLinkTapped(_ gesture: UITapGestureRecognizer) { - if areTouches(inActionString: touches) { - if let action = actionBlock { + for clause in clauses { + if gesture.didTapAttributedTextInLabel(self, inRange: clause.range), let action = clause.actionBlock { action() } } } + override open func touchesEnded(_ touches: Set, with event: UIEvent?) { + + if areTouches(inActionString: touches), let action = actionBlock { + action() + } + } + private func areTouches(inActionString touches: Set?) -> Bool { if UIAccessibility.isVoiceOverRunning || makeWholeViewClickable { @@ -331,8 +383,8 @@ extension Label { } let location: CGPoint? = touches?.first?.location(in: self) - let index: Int = NSRange(location: frontText?.count ?? 0, length: actionText?.count ?? 0).location - let rangeArray = getRangeArrayOfWords(in: actionText, withInitalIndex: index) + let actionTextIndex: Int = NSRange(location: frontText?.count ?? 0, length: actionText?.count ?? 0).location + let rangeArray = getRangeArrayOfWords(in: actionText, withInitalIndex: actionTextIndex) let rectArray = getRectArray(from: rangeArray) as? [NSValue] ?? [] for rect in rectArray { @@ -342,7 +394,7 @@ extension Label { return true } else if wordRect.origin == .zero && wordRect.size.height == 0 && wordRect.size.width == 0 { - // Incase word rect is not found for any reason, make the whole label to be clicable to avoid non functioning link in production. + // In case word rect is not found for any reason, make the whole label to be clicable to avoid non functioning link in production. return true } } @@ -350,6 +402,7 @@ extension Label { return false } + // Works for LabelWithInternalButton private func getRangeArrayOfWords(in string: String?, withInitalIndex index: Int) -> [Any]? { var index = index @@ -368,6 +421,7 @@ extension Label { return rangeArray } + // Works for LabelWithInternalButton private func getRectArray(from rangeArray: [Any]?) -> [Any]? { var rectArray = [AnyHashable]() @@ -380,3 +434,34 @@ extension Label { return rectArray } } + + +extension UITapGestureRecognizer { + + func didTapAttributedTextInLabel(_ label: Label, inRange targetRange: NSRange) -> Bool { + + let layoutManager = NSLayoutManager() + let textContainer = NSTextContainer(size: .zero) + let textStorage = NSTextStorage(attributedString: label.attributedText!) + + layoutManager.addTextContainer(textContainer) + textStorage.addLayoutManager(layoutManager) + + textContainer.lineFragmentPadding = 0.0 + textContainer.lineBreakMode = label.lineBreakMode + textContainer.maximumNumberOfLines = label.numberOfLines + let labelSize = label.bounds.size + textContainer.size = labelSize + + // Find the tapped character location and compare it to the specified range +// let locationOfTouchInLabel = location(in: label) +// let textBoundingBox = layoutManager.usedRect(for: textContainer) +// let textContainerOffset = CGPoint(x: (labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x, +// y: (labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y) +// let locationOfTouchInTextContainer = CGPoint(x: locationOfTouchInLabel.x - textContainerOffset.x, +// y: locationOfTouchInLabel.y - textContainerOffset.y) + let indexOfCharacter = layoutManager.characterIndex(for: location(in: label), in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil) + + return NSLocationInRange(indexOfCharacter, targetRange) + } +} diff --git a/MVMCoreUI/Molecules/TwoButtonView.swift b/MVMCoreUI/Molecules/TwoButtonView.swift index 66b7210a..c99de733 100644 --- a/MVMCoreUI/Molecules/TwoButtonView.swift +++ b/MVMCoreUI/Molecules/TwoButtonView.swift @@ -44,8 +44,8 @@ import UIKit if let backgroundColorString = json?.optionalStringForKey(KeyBackgroundColor) { backgroundColor = UIColor.mfGet(forHex: backgroundColorString) } - let primaryButtonMap = json?.optionalDictionaryForKey("primaryButton") - let secondaryButtonMap = json?.optionalDictionaryForKey("secondaryButton") + let primaryButtonMap = json?.optionalDictionaryForKey(KeyPrimaryButton) + let secondaryButtonMap = json?.optionalDictionaryForKey(KeySecondaryButton) set(primaryButtonJSON: primaryButtonMap, secondaryButtonJSON: secondaryButtonMap, delegateObject: delegateObject, additionalData: additionalData) }