LabelWithInternalButton now properly relies on Label for its actions. Reworked makeWholeViewClickable.

This commit is contained in:
Christiano, Kevin 2019-05-04 16:34:08 -04:00
parent 51820794db
commit b7e99e9e5b
2 changed files with 144 additions and 229 deletions

View File

@ -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<UITouch>, with event: UIEvent?) {
if areTouches(inActionString: touches), let action = actionBlock {
action()
}
}
// For LabelWithInternalButton
private func areTouches(inActionString touches: Set<UITouch>?) -> 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)
}
}

View File

@ -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()
}