LabelWithInternalButton now properly relies on Label for its actions. Reworked makeWholeViewClickable.
This commit is contained in:
parent
51820794db
commit
b7e99e9e5b
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user