Multi-action-text label WIP.
This commit is contained in:
parent
dc6f0d48d4
commit
f64c793aeb
@ -20,6 +20,9 @@ public typealias ActionBlock = () -> Void
|
|||||||
var actionText: String?
|
var actionText: String?
|
||||||
var frontText: String?
|
var frontText: String?
|
||||||
|
|
||||||
|
var clauses = [ActionableClause]()
|
||||||
|
|
||||||
|
// This is here for LabelWithInternalButton
|
||||||
var makeWholeViewClickable = false
|
var makeWholeViewClickable = false
|
||||||
|
|
||||||
// Set this property if you want updateView to update the font based on this standard and the size passed in.
|
// 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
|
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
|
// 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]?) {
|
@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)
|
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,
|
let attributedString = NSMutableAttributedString(string: labelText, attributes: [NSAttributedString.Key.font: label.font as UIFont,
|
||||||
NSAttributedString.Key.foregroundColor: label.textColor as UIColor])
|
NSAttributedString.Key.foregroundColor: label.textColor as UIColor])
|
||||||
for case let attribute as [String: Any] in attributes {
|
for case let attribute as [String: Any] in attributes {
|
||||||
|
|
||||||
guard let attributeType = attribute.optionalStringForKey(KeyType),
|
guard let attributeType = attribute.optionalStringForKey(KeyType),
|
||||||
let location = attribute["location"] as? Int,
|
let location = attribute["location"] as? Int,
|
||||||
let length = attribute["length"] as? Int
|
let length = attribute["length"] as? Int
|
||||||
@ -210,6 +232,29 @@ public typealias ActionBlock = () -> Void
|
|||||||
attributedString.removeAttribute(.font, range: range)
|
attributedString.removeAttribute(.font, range: range)
|
||||||
attributedString.addAttribute(.font, value: 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:
|
default:
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -308,22 +353,29 @@ public typealias ActionBlock = () -> Void
|
|||||||
}
|
}
|
||||||
|
|
||||||
public func needsToBeConstrained() -> Bool {
|
public func needsToBeConstrained() -> Bool {
|
||||||
return true;
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - UIControl Override
|
// MARK: - UIControl Override
|
||||||
extension Label {
|
extension Label {
|
||||||
|
|
||||||
override open func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
@objc func textLinkTapped(_ gesture: UITapGestureRecognizer) {
|
||||||
|
|
||||||
if areTouches(inActionString: touches) {
|
for clause in clauses {
|
||||||
if let action = actionBlock {
|
if gesture.didTapAttributedTextInLabel(self, inRange: clause.range), let action = clause.actionBlock {
|
||||||
action()
|
action()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override open func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||||
|
|
||||||
|
if areTouches(inActionString: touches), let action = actionBlock {
|
||||||
|
action()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func areTouches(inActionString touches: Set<UITouch>?) -> Bool {
|
private func areTouches(inActionString touches: Set<UITouch>?) -> Bool {
|
||||||
|
|
||||||
if UIAccessibility.isVoiceOverRunning || makeWholeViewClickable {
|
if UIAccessibility.isVoiceOverRunning || makeWholeViewClickable {
|
||||||
@ -331,8 +383,8 @@ extension Label {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let location: CGPoint? = touches?.first?.location(in: self)
|
let location: CGPoint? = touches?.first?.location(in: self)
|
||||||
let index: Int = NSRange(location: frontText?.count ?? 0, length: actionText?.count ?? 0).location
|
let actionTextIndex: Int = NSRange(location: frontText?.count ?? 0, length: actionText?.count ?? 0).location
|
||||||
let rangeArray = getRangeArrayOfWords(in: actionText, withInitalIndex: index)
|
let rangeArray = getRangeArrayOfWords(in: actionText, withInitalIndex: actionTextIndex)
|
||||||
let rectArray = getRectArray(from: rangeArray) as? [NSValue] ?? []
|
let rectArray = getRectArray(from: rangeArray) as? [NSValue] ?? []
|
||||||
|
|
||||||
for rect in rectArray {
|
for rect in rectArray {
|
||||||
@ -342,7 +394,7 @@ extension Label {
|
|||||||
return true
|
return true
|
||||||
|
|
||||||
} else if wordRect.origin == .zero && wordRect.size.height == 0 && wordRect.size.width == 0 {
|
} 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
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -350,6 +402,7 @@ extension Label {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Works for LabelWithInternalButton
|
||||||
private func getRangeArrayOfWords(in string: String?, withInitalIndex index: Int) -> [Any]? {
|
private func getRangeArrayOfWords(in string: String?, withInitalIndex index: Int) -> [Any]? {
|
||||||
|
|
||||||
var index = index
|
var index = index
|
||||||
@ -368,6 +421,7 @@ extension Label {
|
|||||||
return rangeArray
|
return rangeArray
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Works for LabelWithInternalButton
|
||||||
private func getRectArray(from rangeArray: [Any]?) -> [Any]? {
|
private func getRectArray(from rangeArray: [Any]?) -> [Any]? {
|
||||||
|
|
||||||
var rectArray = [AnyHashable]()
|
var rectArray = [AnyHashable]()
|
||||||
@ -380,3 +434,34 @@ extension Label {
|
|||||||
return rectArray
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -44,8 +44,8 @@ import UIKit
|
|||||||
if let backgroundColorString = json?.optionalStringForKey(KeyBackgroundColor) {
|
if let backgroundColorString = json?.optionalStringForKey(KeyBackgroundColor) {
|
||||||
backgroundColor = UIColor.mfGet(forHex: backgroundColorString)
|
backgroundColor = UIColor.mfGet(forHex: backgroundColorString)
|
||||||
}
|
}
|
||||||
let primaryButtonMap = json?.optionalDictionaryForKey("primaryButton")
|
let primaryButtonMap = json?.optionalDictionaryForKey(KeyPrimaryButton)
|
||||||
let secondaryButtonMap = json?.optionalDictionaryForKey("secondaryButton")
|
let secondaryButtonMap = json?.optionalDictionaryForKey(KeySecondaryButton)
|
||||||
set(primaryButtonJSON: primaryButtonMap, secondaryButtonJSON: secondaryButtonMap, delegateObject: delegateObject, additionalData: additionalData)
|
set(primaryButtonJSON: primaryButtonMap, secondaryButtonJSON: secondaryButtonMap, delegateObject: delegateObject, additionalData: additionalData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user