From d1153d4bf84c9f1a50fe6ba614017e32c4d7498f Mon Sep 17 00:00:00 2001 From: "Christiano, Kevin" Date: Thu, 2 May 2019 14:12:41 -0400 Subject: [PATCH] Added button delegate checks for labelWithInternalButton. WIP with multi-action label. --- MVMCoreUI/Atoms/Views/Label.swift | 122 +++++++++++++----- .../Atoms/Views/LabelWithInternalButton.swift | 36 ++++-- 2 files changed, 115 insertions(+), 43 deletions(-) diff --git a/MVMCoreUI/Atoms/Views/Label.swift b/MVMCoreUI/Atoms/Views/Label.swift index 01ffb3db..793b8aa5 100644 --- a/MVMCoreUI/Atoms/Views/Label.swift +++ b/MVMCoreUI/Atoms/Views/Label.swift @@ -11,17 +11,16 @@ import MVMCore public typealias ActionBlock = () -> Void -@objcMembers open class Label: UILabel, MVMCoreViewProtocol, MVMCoreUIMoleculeViewProtocol { +@objcMembers open class Label: UILabel, MVMCoreViewProtocol, MFButtonProtocol, MVMCoreUIMoleculeViewProtocol { //------------------------------------------------------ - // MARK: - Properties + // MARK: - General Properties //------------------------------------------------------ + // Specifically used in LabelWithInternalButton to interact with UIControl. var actionBlock: ActionBlock? var actionText: String? var frontText: String? - var clauses = [ActionableClause]() - // This is here for LabelWithInternalButton var makeWholeViewClickable = false @@ -41,14 +40,23 @@ public typealias ActionBlock = () -> Void return !text.isEmpty || !attributedText.string.isEmpty } - struct ActionableClause { + //------------------------------------------------------ + // MARK: - For Multi-Action Text + //------------------------------------------------------ + + private var clauses: [ActionableClause] = [] + + // Used for tappable links in the text. + private struct ActionableClause { + var labelView: Label? var location: Int? var length: Int? var actionText: String? + var actionBlock: ActionBlock? + var words: [String]? { return actionText?.components(separatedBy: " ") } - var actionBlock: ActionBlock? var range: NSRange { return NSRange(location: location ?? 0, length: length ?? 0) @@ -94,7 +102,7 @@ public typealias ActionBlock = () -> Void } //------------------------------------------------------ - // MARK: - Functions + // MARK: - Factory Functions //------------------------------------------------------ @objc public static func commonLabelH1(_ scale: Bool) -> Label { @@ -153,7 +161,46 @@ public typealias ActionBlock = () -> Void // MARK: - Functions //------------------------------------------------------ - @objc public static func setLabel(_ label: UILabel?, withHTML html: String?) { + /** + 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. + */ + @objc public func addTappableLinkAttribute(range: NSRange, actionBlock: @escaping ActionBlock) { + + guard let text = self.text, + let subStringRange = Range(range, in: text) + else { return } + + clauses.append(ActionableClause(labelView: self, + location: range.location, + length: range.length, + actionText: String(text[subStringRange]), + actionBlock: actionBlock)) + + Label.setGestureInteraction(for: self) + } + + /** + Makes the view interactive and applies the gesture recognizer. + + - Parameters: + - label: The current label view that will have an actionable range of text. + */ + private static func setGestureInteraction(for label: Label) { + + if !label.isUserInteractionEnabled { + label.isUserInteractionEnabled = true + let tapGesture = UITapGestureRecognizer(target: label, action: #selector(textLinkTapped(_:))) + tapGesture.numberOfTapsRequired = 1 + label.addGestureRecognizer(tapGesture) + } + } + + @objc public static func setLabel(_ label: UILabel?, with html: String?) { guard let data = html?.data(using: .utf8) else { return } @@ -175,7 +222,7 @@ public typealias ActionBlock = () -> Void label.text = json?.optionalStringForKey(KeyText) - setLabel(label, withHTML: json?.optionalStringForKey("html")) + setLabel(label, with: json?.optionalStringForKey("html")) if let textColorHex = json?.optionalStringForKey(KeyTextColor), !textColorHex.isEmpty { label.textColor = UIColor.mfGet(forHex: textColorHex) @@ -234,25 +281,30 @@ public typealias ActionBlock = () -> Void } case "actions": let actions = attribute.arrayForKey("actions") - let text = json?.optionalStringForKey(KeyText) + guard let text = json?.optionalStringForKey(KeyText) else { continue } for case let action as [String: Any] in actions { - guard let locationx = action["location"] as? Int, - let lengthx = action["length"] as? Int + guard let actionLocation = action["location"] as? Int, + let actionLength = action["length"] as? Int, + let subStringRange = Range(NSRange(location: actionLocation, length: actionLength), in: text) 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) - } + label.clauses.append(ActionableClause(labelView: label, + location: actionLocation, + length: actionLength, + actionText: String(text[subStringRange]), + actionBlock: { [weak delegate] in + var willPerform = true + + if let buttonDelegate = (delegate as? MVMCoreUIDelegateObject)?.buttonDelegate, + buttonDelegate.responds(to: #selector(ButtonObjectDelegate.button(_:shouldPerformActionWithMap:additionalData:))) { + willPerform = buttonDelegate.button?(label, shouldPerformActionWithMap: json, additionalData: additionalData) ?? false + } + + if willPerform { + MVMCoreActionHandler.shared()?.handleAction(with: attribute, additionalData: additionalData, delegateObject: delegate) + } })) + Label.setGestureInteraction(for: label) } default: continue @@ -341,10 +393,10 @@ public typealias ActionBlock = () -> Void standardFontSize = 0 } } - - //------------------------------------------------------ - // MARK: - Atomization - //------------------------------------------------------ +} + +// MARK: - Atomization +extension Label { @objc public func setWithJSON(_ json: [AnyHashable: Any]?, delegateObject: DelegateObject?, additionalData: [AnyHashable: Any]?) { Label.setUILabel(self, withJSON: json, delegate: delegateObject, additionalData: additionalData) @@ -356,7 +408,8 @@ public typealias ActionBlock = () -> Void } } -// MARK: - UIControl Override + +// MARK: - UIControl functionality Override extension Label { @objc func textLinkTapped(_ gesture: UITapGestureRecognizer) { @@ -368,6 +421,7 @@ extension Label { } } + // For LabelWithInternalButton override open func touchesEnded(_ touches: Set, with event: UIEvent?) { if areTouches(inActionString: touches), let action = actionBlock { @@ -375,6 +429,7 @@ extension Label { } } + // For LabelWithInternalButton private func areTouches(inActionString touches: Set?) -> Bool { if UIAccessibility.isVoiceOverRunning || makeWholeViewClickable { @@ -401,7 +456,7 @@ extension Label { return false } - // Works for LabelWithInternalButton + // For LabelWithInternalButton private func getRangeArrayOfWords(in string: String?, withInitalIndex index: Int) -> [Any]? { var index = index @@ -420,7 +475,7 @@ extension Label { return rangeArray } - // Works for LabelWithInternalButton + // For LabelWithInternalButton private func getRectArray(from rangeArray: [Any]?) -> [Any]? { var rectArray = [AnyHashable]() @@ -434,13 +489,16 @@ extension Label { } } +// MARK: - UITapGestureRecognizer Override extension UITapGestureRecognizer { func didTapAttributedTextInLabel(_ label: Label, inRange targetRange: NSRange) -> Bool { + guard let attributedText = label.attributedText else { return false } + let layoutManager = NSLayoutManager() let textContainer = NSTextContainer(size: .zero) - let textStorage = NSTextStorage(attributedString: label.attributedText!) + let textStorage = NSTextStorage(attributedString: attributedText) layoutManager.addTextContainer(textContainer) textStorage.addLayoutManager(layoutManager) diff --git a/MVMCoreUI/Atoms/Views/LabelWithInternalButton.swift b/MVMCoreUI/Atoms/Views/LabelWithInternalButton.swift index 3daeb3b9..e2db0fdb 100644 --- a/MVMCoreUI/Atoms/Views/LabelWithInternalButton.swift +++ b/MVMCoreUI/Atoms/Views/LabelWithInternalButton.swift @@ -146,11 +146,7 @@ public typealias CoreObjectActionLoadPresentDelegate = MVMCoreActionDelegateProt } public convenience init(actionMap: [AnyHashable: Any]?, additionalData: [AnyHashable: Any]?, delegateObject: DelegateObject?) { - self.init(frontText: actionMap?.optionalStringForKey(KeyTitlePrefix), - actionText: actionMap?.optionalStringForKey(KeyTitle), - backText: actionMap?.optionalStringForKey(KeyTitlePostfix), - actionMap: actionMap, additionalData: additionalData, - delegateObject: delegateObject) + self.init(frontText: actionMap?.optionalStringForKey(KeyTitlePrefix), actionText: actionMap?.optionalStringForKey(KeyTitle), backText: actionMap?.optionalStringForKey(KeyTitlePostfix), actionMap: actionMap, additionalData: additionalData, delegateObject: delegateObject) } public convenience init(frontText: String?, backText: String?, actionMap: [AnyHashable: Any]?, additionalData: [AnyHashable: Any]?, delegateObject: DelegateObject?) { @@ -173,8 +169,17 @@ public typealias CoreObjectActionLoadPresentDelegate = MVMCoreActionDelegateProt setText(fullText, startTag: startTag, endTag: endTag) - actionBlock = { - MVMCoreActionHandler.shared()?.handleAction(with: actionMap, additionalData: additionalData, delegateObject: delegateObject) + actionBlock = { [weak self, weak delegateObject] in + var performAction = true + + if let wSelf = self, let wButtonDelegate = (delegateObject as? MVMCoreUIDelegateObject)?.buttonDelegate, + wButtonDelegate.responds(to: #selector(ButtonObjectDelegate.button(_:shouldPerformActionWithMap:additionalData:))) { + performAction = wButtonDelegate.button?(wSelf, shouldPerformActionWithMap: actionMap, additionalData: additionalData) ?? false + } + + if performAction { + MVMCoreActionHandler.shared()?.handleAction(with: actionMap, additionalData: additionalData, delegateObject: delegateObject) + } } } @@ -406,8 +411,17 @@ public typealias CoreObjectActionLoadPresentDelegate = MVMCoreActionDelegateProt actionText = actionMap?.optionalStringForKey(KeyTitle) backText = actionMap?.optionalStringForKey(KeyTitlePostfix) - actionBlock = { - MVMCoreActionHandler.shared()?.handleAction(with: actionMap, additionalData: additionalData, delegateObject: delegateObject) + actionBlock = { [weak self, weak delegateObject] in + var performAction = true + + if let wSelf = self, let wButtonDelegate = (delegateObject as? MVMCoreUIDelegateObject)?.buttonDelegate, + wButtonDelegate.responds(to: #selector(ButtonObjectDelegate.button(_:shouldPerformActionWithMap:additionalData:))) { + performAction = wButtonDelegate.button?(wSelf, shouldPerformActionWithMap: actionMap, additionalData: additionalData) ?? false + } + + if performAction { + MVMCoreActionHandler.shared()?.handleAction(with: actionMap, additionalData: additionalData, delegateObject: delegateObject) + } } text = getTextFromStringComponents() @@ -501,7 +515,7 @@ public typealias CoreObjectActionLoadPresentDelegate = MVMCoreActionDelegateProt @available(*, deprecated) private func setActionMap(_ actionMap: [AnyHashable: Any]?, additionalData: [AnyHashable: Any]?, actionDelegate delegate: ActionObjectDelegate?, buttonDelegate: ButtonObjectDelegate?) { - actionBlock = { [weak self] in + actionBlock = { [weak self, weak buttonDelegate] in var performAction = true if let wSelf = self, let wButtonDelegate = buttonDelegate, wButtonDelegate.responds(to: #selector(ButtonObjectDelegate.button(_:shouldPerformActionWithMap:additionalData:))) { @@ -621,7 +635,7 @@ public typealias CoreObjectActionLoadPresentDelegate = MVMCoreActionDelegateProt actionText = actionMap?.optionalStringForKey(KeyTitle) backText = actionMap?.optionalStringForKey(KeyTitlePostfix) - actionBlock = { + actionBlock = { [weak delegate] in MVMCoreActionHandler.shared()?.handleAction(with: actionMap, additionalData: additionalData, delegate: delegate as? CoreObjectActionLoadPresentDelegate) }