From b7e99e9e5b508e9ba21e721e5dca26c487415dfb Mon Sep 17 00:00:00 2001 From: "Christiano, Kevin" Date: Sat, 4 May 2019 16:34:08 -0400 Subject: [PATCH] LabelWithInternalButton now properly relies on Label for its actions. Reworked makeWholeViewClickable. --- MVMCoreUI/Atoms/Views/Label.swift | 343 +++++++----------- .../Atoms/Views/LabelWithInternalButton.swift | 30 +- 2 files changed, 144 insertions(+), 229 deletions(-) diff --git a/MVMCoreUI/Atoms/Views/Label.swift b/MVMCoreUI/Atoms/Views/Label.swift index 3431a1fc..b229f62a 100644 --- a/MVMCoreUI/Atoms/Views/Label.swift +++ b/MVMCoreUI/Atoms/Views/Label.swift @@ -16,13 +16,8 @@ public typealias ActionBlock = () -> Void // MARK: - General Properties //------------------------------------------------------ - // Specifically used in LabelWithInternalButton to interact with UIControl. - var actionBlock: ActionBlock? - var actionText: String? - var frontText: String? - // This is here for LabelWithInternalButton - var makeWholeViewClickable = false + public var makeWholeViewClickable = false // TODO: TEST this ! // 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 @@ -44,25 +39,33 @@ public typealias ActionBlock = () -> Void // MARK: - For Multi-Action Text //------------------------------------------------------ - private var clauses: [ActionableClause] = [] - var didSetGestureRecognizer = false + public var clauses: [ActionableClause] = [] { + didSet { + if !didSetGestureRecognizer { + isUserInteractionEnabled = true + didSetGestureRecognizer = true + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(textLinkTapped(_:))) + tapGesture.numberOfTapsRequired = 1 + addGestureRecognizer(tapGesture) + } + } + } + // Used for tappable links in the text. - private struct ActionableClause { + public struct ActionableClause { var location: Int? var length: Int? var actionText: String? var actionBlock: ActionBlock? - + var range: NSRange { return NSRange(location: location ?? 0, length: length ?? 0) } func performAction() { - if let action = actionBlock { - action() - } + actionBlock?() } } @@ -158,139 +161,12 @@ public typealias ActionBlock = () -> Void // MARK: - Tappable Methods //------------------------------------------------------ - /** - 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(location: range.location, - length: range.length, - actionText: String(text[subStringRange]), - actionBlock: actionBlock)) - - Label.setGestureInteraction(for: self) - } - /** - 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: - */ - @objc public func addTappableLinkAttribute(range: NSRange, actionMap: [AnyHashable: Any]?, delegate: DelegateObject?, additionalData: [AnyHashable: Any]?) { - - guard let text = self.text, - let subStringRange = Range(range, in: text) - else { return } - - clauses.append(ActionableClause(location: range.location, - length: range.length, - actionText: String(text[subStringRange]), - actionBlock: { [weak self, weak delegate] in - var willPerform = true - - if let wSelf = self, let buttonDelegate = (delegate as? MVMCoreUIDelegateObject)?.buttonDelegate, - buttonDelegate.responds(to: #selector(ButtonObjectDelegate.button(_:shouldPerformActionWithMap:additionalData:))) { - willPerform = buttonDelegate.button?(wSelf, shouldPerformActionWithMap: actionMap, additionalData: additionalData) ?? false - } - - if willPerform { - MVMCoreActionHandler.shared()?.handleAction(with: actionMap, additionalData: additionalData, delegateObject: delegate) - } })) - - Label.setGestureInteraction(for: self) - } - - /** - Provides an actionable range of text. - - Allows actionable range to be established by a particular substring of the containing label text. - - - Attention: This method expects text to be set first. Otherwise, it will do nothing. Do not use if actionText is not unique in the Label's text. - - Parameters: - - actionText: The actionable text contained witin the label's text. - - actionBlock: The code triggered when tapping the range of text. - */ - @objc public func addTappableLinkAttribute(actionText: String, actionBlock: @escaping ActionBlock) { - - guard let text = self.text else { return } - let string = text as NSString - let range = string.range(of: actionText) - - clauses.append(ActionableClause(location: range.location, - length: range.length, - actionText: actionText, - actionBlock: actionBlock)) - - Label.setGestureInteraction(for: self) - } - - /** - Provides an actionable range of text. - - - Attention: This method expects text to be set first. Otherwise, it will do nothing. Do not use if actionText is not unique in the Label's text. - - Parameters: - - actionText: The actionable text contained witin the label's text. - - actionMap: - - delegate: - - additionalData: - */ - @objc public func addTappableLinkAttribute(actionText: String, actionMap: [AnyHashable: Any]?, delegate: DelegateObject?, additionalData: [AnyHashable: Any]?) { - - guard let text = self.text else { return } - let string = text as NSString - let range = string.range(of: actionText) - - clauses.append(ActionableClause(location: range.location, length: range.length, actionText: actionText, - actionBlock: { [weak self, weak delegate] in - var willPerform = true - - if let wSelf = self, let buttonDelegate = (delegate as? MVMCoreUIDelegateObject)?.buttonDelegate, - buttonDelegate.responds(to: #selector(ButtonObjectDelegate.button(_:shouldPerformActionWithMap:additionalData:))) { - willPerform = buttonDelegate.button?(wSelf, shouldPerformActionWithMap: actionMap, additionalData: additionalData) ?? false - } - - if willPerform { - MVMCoreActionHandler.shared()?.handleAction(with: actionMap, additionalData: additionalData, delegateObject: delegate) - } })) - - Label.setGestureInteraction(for: self) - } //------------------------------------------------------ // MARK: - Functions //------------------------------------------------------ - /** - 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.didSetGestureRecognizer { - label.isUserInteractionEnabled = true - label.didSetGestureRecognizer = true - let tapGesture = UITapGestureRecognizer(target: label, action: #selector(textLinkTapped(_:))) - tapGesture.numberOfTapsRequired = 1 - label.addGestureRecognizer(tapGesture) - } - } - /** Makes the view interactive and applies the gesture recognizer. @@ -410,7 +286,6 @@ public typealias ActionBlock = () -> Void if willPerform { MVMCoreActionHandler.shared()?.handleAction(with: json, additionalData: additionalData, delegateObject: delegate) } })) - Label.setGestureInteraction(for: label) } default: continue @@ -515,90 +390,126 @@ extension Label { } -// MARK: - UIControl functionality Override +// MARK: - Multi-Action Functionality 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. + */ + @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(location: range.location, + length: range.length, + actionText: String(text[subStringRange]), + actionBlock: actionBlock)) + } + + /** + 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: + */ + @objc public func addTappableLinkAttribute(range: NSRange, actionMap: [AnyHashable: Any]?, delegate: DelegateObject?, additionalData: [AnyHashable: Any]?) { + + guard let text = self.text, + let subStringRange = Range(range, in: text) + else { return } + + clauses.append(ActionableClause(location: range.location, + length: range.length, + actionText: String(text[subStringRange]), + actionBlock: { [weak self, weak delegate] in + var willPerform = true + + if let wSelf = self, let buttonDelegate = (delegate as? MVMCoreUIDelegateObject)?.buttonDelegate, + buttonDelegate.responds(to: #selector(ButtonObjectDelegate.button(_:shouldPerformActionWithMap:additionalData:))) { + willPerform = buttonDelegate.button?(wSelf, shouldPerformActionWithMap: actionMap, additionalData: additionalData) ?? false + } + + if willPerform { + MVMCoreActionHandler.shared()?.handleAction(with: actionMap, additionalData: additionalData, delegateObject: delegate) + } })) + } + + /** + Provides an actionable range of text. + + Allows actionable range to be established by a particular substring of the containing label text. + + - Attention: This method expects text to be set first. Otherwise, it will do nothing. Do not use if actionText is not unique in the Label's text. + - Parameters: + - actionText: The actionable text contained witin the label's text. + - actionBlock: The code triggered when tapping the range of text. + */ + @objc public func addTappableLinkAttribute(actionText: String, actionBlock: @escaping ActionBlock) { + + guard let text = self.text else { return } + let string = text as NSString + let range = string.range(of: actionText) + + clauses.append(ActionableClause(location: range.location, + length: range.length, + actionText: actionText, + actionBlock: actionBlock)) + } + + /** + Provides an actionable range of text. + + - Attention: This method expects text to be set first. Otherwise, it will do nothing. Do not use if actionText is not unique in the Label's text. + - Parameters: + - actionText: The actionable text contained witin the label's text. + - actionMap: + - delegate: + - additionalData: + */ + @objc public func addTappableLinkAttribute(actionText: String, actionMap: [AnyHashable: Any]?, delegate: DelegateObject?, additionalData: [AnyHashable: Any]?) { + + guard let text = self.text else { return } + let string = text as NSString + let range = string.range(of: actionText) + + clauses.append(ActionableClause(location: range.location, length: range.length, actionText: actionText, + actionBlock: { [weak self, weak delegate] in + var willPerform = true + + if let wSelf = self, let buttonDelegate = (delegate as? MVMCoreUIDelegateObject)?.buttonDelegate, + buttonDelegate.responds(to: #selector(ButtonObjectDelegate.button(_:shouldPerformActionWithMap:additionalData:))) { + willPerform = buttonDelegate.button?(wSelf, shouldPerformActionWithMap: actionMap, additionalData: additionalData) ?? false + } + + if willPerform { + MVMCoreActionHandler.shared()?.handleAction(with: actionMap, additionalData: additionalData, delegateObject: delegate) + } })) + } + @objc private func textLinkTapped(_ gesture: UITapGestureRecognizer) { for clause in clauses { - if gesture.didTapAttributedTextInLabel(self, inRange: clause.range) { + if gesture.didTapAttributedTextInLabel(self, inRange: clause.range, isWholeViewTappable: makeWholeViewClickable) { clause.performAction() } } } - - // For LabelWithInternalButton - override open func touchesEnded(_ touches: Set, with event: UIEvent?) { - - if areTouches(inActionString: touches), let action = actionBlock { - action() - } - } - - // For LabelWithInternalButton - private func areTouches(inActionString touches: Set?) -> Bool { - - if UIAccessibility.isVoiceOverRunning || makeWholeViewClickable { - return true - } - - let location: CGPoint? = touches?.first?.location(in: self) - 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 { - let wordRect: CGRect = rect.cgRectValue - - if let position = location, wordRect.contains(position) { - return true - - } else if wordRect.origin == .zero && wordRect.size.height == 0 && wordRect.size.width == 0 { - // 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 - } - } - - return false - } - - // For LabelWithInternalButton - private func getRangeArrayOfWords(in string: String?, withInitalIndex index: Int) -> [Any]? { - - var index = index - let words = string?.components(separatedBy: " ") ?? [] - var rangeArray = [AnyHashable]() - - for word in words { - let finalWord = word + " " - let length: Int = finalWord.count - let wordRange = NSRange(location: index, length: length) - let rangeValue = NSValue(range: wordRange) - rangeArray.append(rangeValue) - index += length - } - - return rangeArray - } - - // For LabelWithInternalButton - private func getRectArray(from rangeArray: [Any]?) -> [Any]? { - - var rectArray = [AnyHashable]() - - for range in rangeArray as? [NSValue] ?? [] { - let rectValue = NSValue(cgRect: boundingRect(forCharacterRange: range.rangeValue)) - rectArray.append(rectValue) - } - - return rectArray - } } -// MARK: - UITapGestureRecognizer Override extension UITapGestureRecognizer { - func didTapAttributedTextInLabel(_ label: Label, inRange targetRange: NSRange) -> Bool { + func didTapAttributedTextInLabel(_ label: Label, inRange targetRange: NSRange, isWholeViewTappable: Bool) -> Bool { guard let attributedText = label.attributedText else { return false } @@ -617,6 +528,12 @@ extension UITapGestureRecognizer { let indexOfCharacter = layoutManager.characterIndex(for: location(in: label), in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil) - return NSLocationInRange(indexOfCharacter, targetRange) + var range = targetRange + + if isWholeViewTappable, let wholeRange = NSRange(attributedText.string) { + range = wholeRange + } + + return NSLocationInRange(indexOfCharacter, range) } } diff --git a/MVMCoreUI/Atoms/Views/LabelWithInternalButton.swift b/MVMCoreUI/Atoms/Views/LabelWithInternalButton.swift index 2bdab94a..c5981e5e 100644 --- a/MVMCoreUI/Atoms/Views/LabelWithInternalButton.swift +++ b/MVMCoreUI/Atoms/Views/LabelWithInternalButton.swift @@ -61,7 +61,14 @@ public typealias CoreObjectActionLoadPresentDelegate = MVMCoreActionDelegateProt public var actionBlock: ActionBlock? { willSet(newActionBlock) { - label?.actionBlock = newActionBlock + if newActionBlock == nil { + label?.clauses = [] + } else { + label?.clauses = [Label.ActionableClause(location: actionRange.location, + length: actionRange.length, + actionText: actionText, + actionBlock: newActionBlock)] + } } } @@ -69,6 +76,7 @@ public typealias CoreObjectActionLoadPresentDelegate = MVMCoreActionDelegateProt return NSRange(location: frontText?.count ?? 0, length: actionText?.count ?? 0) } + // Makes entire range of text clickable public var makeWholeViewClickable = false { willSet(newBool) { label?.makeWholeViewClickable = newBool @@ -81,18 +89,8 @@ public typealias CoreObjectActionLoadPresentDelegate = MVMCoreActionDelegateProt } } - public var frontText: String? { - willSet(newFrontText) { - label?.frontText = newFrontText - } - } - - public var actionText: String? { - willSet(newActionText) { - label?.actionText = newActionText - } - } - + public var frontText: String? + public var actionText: String? public var backText: String? private var internalText: String = "" @@ -209,8 +207,8 @@ public typealias CoreObjectActionLoadPresentDelegate = MVMCoreActionDelegateProt // Adding the underline setAlternateActionTextAttributes([NSAttributedString.Key.underlineStyle: NSNumber(value: NSUnderlineStyle.single.rawValue)]) - self.label?.attributedText = attributedText - self.label?.accessibilityTraits = actionText?.isEmpty ?? false ? .staticText : .button + label?.attributedText = attributedText + label?.accessibilityTraits = actionText?.isEmpty ?? false ? .staticText : .button } @objc public func setActionMap(_ actionMap: [AnyHashable: Any]?, additionalData: [AnyHashable: Any]?, delegateObject: DelegateObject?) { @@ -538,10 +536,10 @@ public typealias CoreObjectActionLoadPresentDelegate = MVMCoreActionDelegateProt createLabel() self.frontText = frontText - setActionMap(actionMap, additionalData: additionalData, actionDelegate: delegate, buttonDelegate: buttonDelegate) self.actionText = actionText self.backText = backText text = getTextFromStringComponents() + setActionMap(actionMap, additionalData: additionalData, actionDelegate: delegate, buttonDelegate: buttonDelegate) setLabelAttributes() }