refactored label for first cut, pretty big changes though....

Signed-off-by: Matt Bruce <matt.bruce@verizon.com>
This commit is contained in:
Matt Bruce 2024-02-12 17:32:06 -06:00
parent 29f43b7861
commit ddb15e72f3

View File

@ -8,15 +8,18 @@
//
import MVMCore
import VDS
public typealias ActionBlock = () -> ()
@objcMembers open class Label: UILabel, MVMCoreViewProtocol, MoleculeViewProtocol, MVMCoreUIViewConstrainingProtocol, MFButtonProtocol, ViewMaskingProtocol {
@objcMembers open class Label: VDS.Label, VDSMoleculeViewProtocol, MVMCoreUIViewConstrainingProtocol, MFButtonProtocol, ViewMaskingProtocol {
//------------------------------------------------------
// MARK: - Properties
//------------------------------------------------------
open var viewModel: LabelModel!
open var delegateObject: MVMCoreUIDelegateObject?
open var additionalData: [AnyHashable : Any]?
public var makeWholeViewClickable = false
@ -30,52 +33,15 @@ public typealias ActionBlock = () -> ()
/// A specific text index to use as a unique marker.
public var hero: Int?
// Used for scaling the font in updateView.
private var originalAttributedString: NSAttributedString?
public var hasText: Bool {
guard let text = text, let attributedText = attributedText else { return false }
return !text.isEmpty || !attributedText.string.isEmpty
}
public var getRange: NSRange {
NSRange(location: 0, length: text?.count ?? 0)
}
public var shouldMaskWhileRecording: Bool = false
public var model: MoleculeModelProtocol?
//------------------------------------------------------
// MARK: - Multi-Action Text
//------------------------------------------------------
/// Data store of the tappable ranges of the text.
public var clauses: [ActionableClause] = [] {
didSet {
isUserInteractionEnabled = !clauses.isEmpty
if clauses.count > 1 {
clauses.sort { first, second in
return first.range.location < second.range.location
}
}
}
}
/// Used for tappable links in the text.
public struct ActionableClause {
public var range: NSRange
public var actionBlock: ActionBlock
public var accessibilityID: Int = 0
public func performAction() {
actionBlock()
}
public init(range: NSRange, actionBlock: @escaping ActionBlock, accessibilityID: Int = 0) {
self.range = range
self.actionBlock = actionBlock
self.accessibilityID = accessibilityID
}
public var hasText: Bool {
guard let text = text, let attributedText = attributedText else { return false }
return !text.isEmpty || !attributedText.string.isEmpty
}
//------------------------------------------------------
@ -84,54 +50,32 @@ public typealias ActionBlock = () -> ()
/// Sets the clauses array to empty.
@objc public func setEmptyClauses() {
clauses = []
}
//------------------------------------------------------
// MARK: - Initialization
//------------------------------------------------------
@objc public func setupView() {
backgroundColor = .clear
numberOfLines = 0
lineBreakMode = .byWordWrapping
translatesAutoresizingMaskIntoConstraints = false
clauses = []
accessibilityCustomActions = []
accessibilityTraits = .staticText
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(textLinkTapped))
tapGesture.numberOfTapsRequired = 1
addGestureRecognizer(tapGesture)
}
@objc public init() {
super.init(frame: .zero)
setupView()
@objc public required init() {
super.init()
}
@objc required public init?(coder: NSCoder) {
super.init(coder: coder)
setupView()
}
@objc override public init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
public init(fontStyle: Styler.Font, _ scale: Bool = true) {
super.init(frame: .zero)
setupView()
font = fontStyle.getFont(false)
textColor = fontStyle.color()
setScale(scale)
guard let style = fontStyle.vdsTextStyle() else { return }
textStyle = style
}
@objc convenience public init(standardFontSize size: CGFloat) {
self.init()
standardFontSize = size
}
/// Convenience to init Label with a link comprised of range, actionMap and delegateObject
@ -145,7 +89,6 @@ public typealias ActionBlock = () -> ()
required public init(model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
super.init(frame: .zero)
setupView()
styleB2(true)
set(with: model, delegateObject, additionalData)
}
@ -236,12 +179,6 @@ public typealias ActionBlock = () -> ()
}
}
enum LabelAlignment: String {
case center
case right
case left
}
@objc public func resetAttributeStyle() {
/*
* This is to address a reuse issue with iOS 13 and up.
@ -250,294 +187,104 @@ public typealias ActionBlock = () -> ()
* appropriately called.
* Only other reference found of issue: https://www.thetopsites.net/article/58142205.shtml
*/
if let attributedText = attributedText, let text = text, !text.isEmpty {
let attributedString = NSMutableAttributedString(string: text)
let range = NSRange(location: 0, length: text.count)
for attribute in attributedText.attributes(at: 0, effectiveRange: nil) {
if attribute.key == .underlineStyle {
attributedString.addAttribute(.underlineStyle, value: 0, range: range)
}
if attribute.key == .strikethroughStyle {
attributedString.addAttribute(.strikethroughStyle, value: 0, range: range)
}
if let text = text, !text.isEmpty {
//create the primary string
let mutableText = NSMutableAttributedString.mutableText(for: text,
textStyle: textStyle,
useScaledFont: useScaledFont,
textColor: textColorConfiguration.getColor(self),
alignment: textAlignment,
lineBreakMode: lineBreakMode)
if let attributes = attributes {
mutableText.apply(attributes: attributes)
}
self.attributedText = attributedString
self.attributedText = mutableText
}
}
public func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject? = nil, _ additionalData: [AnyHashable: Any]? = nil) {
public func viewModelDidUpdate() {
shouldMaskWhileRecording = viewModel.shouldMaskRecordedView ?? false
text = viewModel.text
hero = viewModel.hero
Label.setLabel(self, withHTML: viewModel.html)
textAlignment = viewModel.textAlignment ?? .left
surface = viewModel.surface
clauses = []
text = nil
attributedText = nil
originalAttributedString = nil
shouldMaskWhileRecording = model.shouldMaskRecordedView ?? false
guard let labelModel = model as? LabelModel else { return }
text = labelModel.text
if let accessibilityTraits = labelModel.accessibilityTraits {
self.accessibilityTraits = accessibilityTraits
}
resetAttributeStyle()
hero = labelModel.hero
Label.setLabel(self, withHTML: labelModel.html)
isAccessibilityElement = hasText
switch labelModel.textAlignment {
case .center:
textAlignment = .center
case .right:
textAlignment = .right
default:
textAlignment = .left
}
makeWholeViewClickable = labelModel.makeWholeViewClickable ?? false
if let backgroundColor = labelModel.backgroundColor {
makeWholeViewClickable = viewModel.makeWholeViewClickable ?? false
if let backgroundColor = viewModel.backgroundColor {
self.backgroundColor = backgroundColor.uiColor
}
if let accessibilityText = labelModel.accessibilityText {
accessibilityLabel = accessibilityText
}
if let fontStyle = labelModel.fontStyle {
fontStyle.styleLabel(self, genericScaling: false)
standardFontSize = font.pointSize
} else {
let fontSize = labelModel.fontSize
if let fontSize = fontSize {
if let style = viewModel.fontStyle?.vdsTextStyle() {
font = style.font
textStyle = style
} else if let fontName = viewModel.fontName {
// there is a TextStyle.defaultStyle
let fontSize = viewModel.fontSize
if let fontSize {
standardFontSize = fontSize
}
if let fontName = labelModel.fontName {
font = MFFonts.mfFont(withName: fontName, size: fontSize ?? standardFontSize)
} else if let fontSize = fontSize {
font = font.updateSize(fontSize)
if let customStyle = style(for: fontName, pointSize: fontSize ?? standardFontSize), customStyle != textStyle {
font = customStyle.font
textStyle = customStyle
}
}
if let color = labelModel.textColor {
textColor = color.uiColor
if let color = viewModel.textColor {
textColorConfiguration = SurfaceColorConfiguration(color.uiColor, color.uiColor).eraseToAnyColorable()
}
if let lines = labelModel.numberOfLines {
numberOfLines = lines
if let lines = viewModel.numberOfLines {
numberOfLines = lines
}
if let attributes = labelModel.attributes, let labelText = text {
let attributedString = NSMutableAttributedString(string: labelText, attributes: [NSAttributedString.Key.font: font.updateSize(standardFontSize), NSAttributedString.Key.foregroundColor: textColor as UIColor])
for attribute in attributes {
guard let range = validateAttribute(range: NSRange(location: attribute.location, length: attribute.length) , in: attributedString, type: attribute.type)
else { continue }
switch attribute {
case let underlineAtt as LabelAttributeUnderlineModel:
attributedString.addAttribute(.underlineStyle, value: underlineAtt.underlineValue.rawValue, range: range)
if let underlineColor = underlineAtt.color?.uiColor {
attributedString.addAttribute(.underlineColor, value: underlineColor, range: range)
}
case _ as LabelAttributeStrikeThroughModel:
attributedString.addAttribute(.strikethroughStyle, value: NSUnderlineStyle.thick.rawValue, range: range)
attributedString.addAttribute(.baselineOffset, value: 0, range: range)
case let colorAtt as LabelAttributeColorModel:
if let colorHex = colorAtt.textColor {
attributedString.removeAttribute(.foregroundColor, range: range)
attributedString.addAttribute(.foregroundColor, value: colorHex.uiColor, range: range)
}
case let imageAtt as LabelAttributeImageModel:
var fontSize = font.pointSize
if let attributeSize = imageAtt.size {
fontSize = attributeSize
}
let imageName = imageAtt.name ?? "externalLink"
let imageAttachment: NSTextAttachment
if let url = imageAtt.URL {
imageAttachment = Label.getTextAttachmentFrom(url: url, dimension: fontSize, label: self)
} else {
imageAttachment = Label.getTextAttachmentImage(name: imageName, dimension: fontSize)
}
// Confirm that the intended image location is within range.
if 0...labelText.count ~= imageAtt.location {
let mutableString = NSMutableAttributedString()
mutableString.append(NSAttributedString(attachment: imageAttachment))
attributedString.insert(mutableString, at: imageAtt.location)
}
case let fontAtt as LabelAttributeFontModel:
if let fontStyle = fontAtt.style {
attributedString.removeAttribute(.font, range: range)
attributedString.removeAttribute(.foregroundColor, range: range)
attributedString.addAttribute(.font, value: fontStyle.getFont(), range: range)
attributedString.addAttribute(.foregroundColor, value: fontStyle.color(), range: range)
} else {
let fontSize = fontAtt.size
var font: UIFont?
if let fontName = fontAtt.name {
font = MFFonts.mfFont(withName: fontName, size: fontSize ?? self.font.pointSize)
} else if let fontSize = fontSize {
font = self.font.updateSize(fontSize)
}
if let font = font {
attributedString.removeAttribute(.font, range: range)
attributedString.addAttribute(.font, value: font, range: range)
}
}
case let actionAtt as LabelAttributeActionModel:
addTappableLinkAttribute(range: NSRange(location: range.location, length: range.length)) {
MVMCoreUIActionHandler.performActionUnstructured(with: actionAtt.action, sourceModel: model, additionalData: additionalData, delegateObject: delegateObject)
}
addActionAttributes(range: range, string: attributedString)
default:
continue
}
}
attributedText = attributedString
originalAttributedString = attributedText
if let attributeModels = viewModel.attributes?.toVDSLabelAttributeModel(delegateObject: delegateObject, additionalData: additionalData) {
attributes = attributeModels
}
self.model = labelModel
}
/// See if the font that is currently set matches a VDS Font and if so grab the matching TextStyle or create custom TextStyle that
/// that the Label will use moving forward.
private func checkforFontChange() {
guard let customStyle = style(for: font.fontName, pointSize: font.pointSize), customStyle != textStyle
else { return }
textStyle = customStyle
}
private func style(for fontName: String, pointSize: CGFloat) -> TextStyle? {
guard let vdsFont = Font.from(fontName: fontName),
let customStyle = TextStyle.style(from: vdsFont, pointSize: pointSize)
else { return nil }
return customStyle
}
open override func updateView() {
checkforFontChange()
super.updateView()
}
open override func updateAccessibility() {
super.updateAccessibility()
if let accessibilityTraits = viewModel?.accessibilityTraits {
self.accessibilityTraits = accessibilityTraits
}
if let accessibilityText = viewModel?.accessibilityText {
accessibilityLabel = accessibilityText
}
}
public func updateView(_ size: CGFloat) { }
@objc public static func setUILabel(_ label: UILabel?, withJSON json: [AnyHashable: Any]?, delegate: DelegateObject?, additionalData: [AnyHashable: Any]?) {
guard let label = label else { return }
// Some properties can only be set on Label.
// Label fonts should not be scaled because it will be scaled in updateView.
let mvmLabel = label as? Label
label.text = json?.optionalStringForKey(KeyText)
setLabel(label, withHTML: json?.optionalStringForKey("html"))
if let alignment = json?.optionalStringForKey("textAlignment") {
switch alignment {
case "center":
label.textAlignment = .center
case "right":
label.textAlignment = .right
default:
label.textAlignment = .left
}
}
mvmLabel?.makeWholeViewClickable = json?.boolForKey("makeWholeViewClickable") ?? false
if let backgroundColorHex = json?.optionalStringForKey(KeyBackgroundColor), !backgroundColorHex.isEmpty {
label.backgroundColor = UIColor.mfGet(forHex: backgroundColorHex)
}
label.accessibilityLabel = json?.optionalStringForKey("accessibilityText")
if let fontStyle = json?.optionalStringForKey("fontStyle") {
MFStyler.style(label: label, styleString: fontStyle, genericScaling: mvmLabel == nil)
mvmLabel?.standardFontSize = label.font.pointSize
} else {
let fontSize = json?["fontSize"] as? CGFloat
if let fontSize = fontSize {
mvmLabel?.standardFontSize = fontSize
}
if let fontName = json?.optionalStringForKey("fontName") {
label.font = MFFonts.mfFont(withName: fontName, size: fontSize ?? mvmLabel?.standardFontSize ?? label.font.pointSize)
} else if let fontSize = fontSize {
label.font = label.font.updateSize(fontSize)
}
}
if let textColorHex = json?.optionalStringForKey(KeyTextColor), !textColorHex.isEmpty {
label.textColor = UIColor.mfGet(forHex: textColorHex)
}
if let attributes = json?.optionalArrayForKey("attributes"), let labelText = label.text {
let attributedString = NSMutableAttributedString(string: labelText,
attributes: [NSAttributedString.Key.font: mvmLabel?.font.updateSize(mvmLabel!.standardFontSize) ?? label.font as UIFont,
NSAttributedString.Key.foregroundColor: label.textColor as UIColor])
for case let attribute as [String: Any] in attributes {
guard let attributeType = attribute.optionalStringForKey(KeyType),
let location = attribute["location"] as? Int,
let length = attribute["length"] as? Int,
let range = validateAttribute(range: NSRange(location: location, length: length), in: attributedString, type: attributeType)
else { continue }
switch attributeType {
case "underline":
attributedString.addAttribute(.underlineStyle, value: NSUnderlineStyle.single.rawValue, range: range)
case "strikethrough":
attributedString.addAttribute(.strikethroughStyle, value: NSUnderlineStyle.thick.rawValue, range: range)
attributedString.addAttribute(.baselineOffset, value: 0, range: range)
case "color":
if let colorHex = attribute.optionalStringForKey(KeyTextColor), !colorHex.isEmpty {
attributedString.removeAttribute(.foregroundColor, range: range)
attributedString.addAttribute(.foregroundColor, value: UIColor.mfGet(forHex: colorHex), range: range)
}
case "image":
let fontSize = attribute["size"] as? CGFloat ?? label.font.pointSize
let imageName = attribute["name"] as? String ?? "externalLink"
let imageURL = attribute["URL"] as? String
let imageAttachment: NSTextAttachment
if let url = imageURL, let label = label as? Label {
imageAttachment = Label.getTextAttachmentFrom(url: url, dimension: fontSize, label: label)
} else {
imageAttachment = Label.getTextAttachmentImage(name: imageName, dimension: fontSize)
}
let mutableString = NSMutableAttributedString()
mutableString.append(NSAttributedString(attachment: imageAttachment))
attributedString.insert(mutableString, at: location)
case "font":
if let fontStyle = attribute.optionalStringForKey("style") {
let styles = MFStyler.getAttributedString(for: "0", styleString: fontStyle, genericScaling: mvmLabel == nil)
attributedString.removeAttribute(.font, range: range)
attributedString.removeAttribute(.foregroundColor, range: range)
attributedString.addAttributes(styles.attributes(at: 0, effectiveRange: nil), range: range)
} else {
let fontSize = attribute["size"] as? CGFloat
var font: UIFont?
if let fontName = attribute.optionalStringForKey("name") {
font = MFFonts.mfFont(withName: fontName, size: fontSize ?? mvmLabel?.standardFontSize ?? label.font.pointSize)
} else if let fontSize = fontSize {
font = label.font.updateSize(fontSize)
}
if let font = font {
attributedString.removeAttribute(.font, range: range)
attributedString.addAttribute(.font, value: font, range: range)
}
}
case "action":
guard let actionLabel = label as? Label else { continue }
actionLabel.addActionAttributes(range: range, string: attributedString)
if let actionBlock = actionLabel.createActionBlockFor(actionMap: attribute, additionalData: additionalData, delegateObject: delegate) {
actionLabel.appendActionableClause(range: range, actionBlock: actionBlock)
}
default:
continue
}
}
label.attributedText = attributedString
mvmLabel?.originalAttributedString = attributedString
}
guard let label = label as? Label,
let json = json as? [String: Any],
let labelModel = try? LabelModel.decode(jsonDict: json) else { return }
label.set(with: labelModel, delegate as? MVMCoreUIDelegateObject, additionalData)
}
//------------------------------------------------------
@ -545,206 +292,53 @@ public typealias ActionBlock = () -> ()
//------------------------------------------------------
public func setFontStyle(_ fontStyle: Styler.Font, _ scale: Bool = true) {
fontStyle.styleLabel(self, genericScaling: false)
setScale(scale)
style(for: fontStyle)
}
//------------------------------------------------------
// MARK: - 2.0 Styling Methods
//------------------------------------------------------
private func style(for legacy: Styler.Font) {
guard let style = legacy.vdsTextStyle() else { return }
textStyle = style
}
@objc public func styleH1(_ scale: Bool) {
MFStyler.styleLabelH1(self, genericScaling: false)
setScale(scale)
style(for: .H1)
}
@objc public func styleH2(_ scale: Bool) {
MFStyler.styleLabelH2(self, genericScaling: false)
setScale(scale)
style(for: .H2)
}
@objc public func styleH3(_ scale: Bool) {
MFStyler.styleLabelH3(self, genericScaling: false)
setScale(scale)
style(for: .H3)
}
@objc public func styleH32(_ scale: Bool) {
MFStyler.styleLabelH32(self, genericScaling: false)
setScale(scale)
style(for: .H32)
}
@objc public func styleB1(_ scale: Bool) {
MFStyler.styleLabelB1(self, genericScaling: false)
setScale(scale)
style(for: .B1)
}
@objc public func styleB2(_ scale: Bool) {
MFStyler.styleLabelB2(self, genericScaling: false)
setScale(scale)
style(for: .B2)
}
@objc public func styleB3(_ scale: Bool) {
MFStyler.styleLabelB3(self, genericScaling: false)
setScale(scale)
style(for: .B3)
}
@objc public func styleB20(_ scale: Bool) {
MFStyler.styleLabelB20(self, genericScaling: false)
setScale(scale)
style(for: .B20)
}
/// Will remove the values contained in attributedText.
func clearAttributes() {
guard let labelText = text, !labelText.isEmpty else { return }
guard let attributes = attributedText?.attributes(at: 0, longestEffectiveRange: nil, in: NSRange(location: 0, length: labelText.count))
else { return }
let attributedString = NSMutableAttributedString(string: labelText)
for attribute in attributes {
attributedString.removeAttribute(attribute.key, range: NSRange(location: 0, length: labelText.count))
}
attributedText = attributedString
}
}
@objc public func updateView(_ size: CGFloat) {
scaleSize = size as NSNumber
if let originalAttributedString = originalAttributedString {
let attributedString = NSMutableAttributedString(attributedString: originalAttributedString)
attributedString.removeAttribute(.font, range: NSRange(location: 0, length: attributedString.length))
// Loop the original attributed string, resize the fonts.
originalAttributedString.enumerateAttribute(.font, in: NSRange(location: 0, length: originalAttributedString.length), options: []) { value, range, stop in
if let fontObj = value as? UIFont, let stylerSize = MFStyler.sizeObjectGeneric(forCurrentDevice: fontObj.pointSize)?.getValueBased(onSize: size) {
attributedString.addAttribute(.font, value: fontObj.updateSize(stylerSize) as Any, range: range)
}
}
// Loop the original attributed string, resize the image attachments.
originalAttributedString.enumerateAttribute(.attachment, in: NSRange(location: 0, length: originalAttributedString.length), options: []) { value, range, stop in
if let attachment = value as? NSTextAttachment,
let stylerSize = MFStyler.sizeObjectGeneric(forCurrentDevice: attachment.bounds.width)?.getValueBased(onSize: size) {
let dimension = round(stylerSize)
attachment.bounds = CGRect(x: 0, y: 0, width: dimension, height: dimension)
}
}
attributedText = attributedString
} else if !MVMCoreGetterUtility.fequal(a: Float(standardFontSize), b: 0.0), let sizeObject = sizeObject ?? MFStyler.sizeObjectGeneric(forCurrentDevice: standardFontSize) {
font = font.updateSize(sizeObject.getValueBased(onSize: size))
}
}
@objc public func setFont(_ font: UIFont, scale: Bool) {
self.font = font
setScale(scale)
}
@objc public func setScale(_ scale: Bool) {
if scale {
standardFontSize = font.pointSize
if let floatScale = scaleSize?.floatValue {
updateView(CGFloat(floatScale))
} else {
updateView(MVMCoreUIUtility.getWidth())
}
} else {
standardFontSize = 0
}
}
/**
Appends an external link image to the end of the attributed string.
Will provide one whitespace to the left of the icon; adds 2 chars to the end of the string.
*/
@objc public func appendExternalLinkIcon() {
guard let attributedText = attributedText else { return }
let mutableString = NSMutableAttributedString(attributedString: attributedText)
mutableString.append(NSAttributedString(string: " "))
mutableString.append(NSAttributedString(attachment: Label.getTextAttachmentImage(dimension: font.pointSize)))
self.attributedText = mutableString
}
/**
Insert external link icon anywhere within text of Label.
- Note: Each icon insertion adds 1 additional characters to the overall text length.
Therefore, you **MUST** insert icons and links in the order they would appear.
- parameter index: Location within the associated text to insert an external Link Icon
*/
public func insertExternalLinkIcon(at index: Int) {
guard let attributedText = attributedText, index <= attributedText.string.count && index >= 0 else { return }
let mutableString = NSMutableAttributedString(attributedString: attributedText)
mutableString.insert(NSAttributedString(attachment: Label.getTextAttachmentImage(dimension: font.pointSize)), at: index)
self.attributedText = mutableString
}
/*
Retrieves an NSTextAttachment for NSAttributedString that is prepped to be inserted with the text.
- parameter name: The Asset name of the image. DEFAULT: "externalLink"
- parameter dimension: length of the height and width of the image. Will be 80% the passed magnitude.
*/
static func getTextAttachmentImage(name: String = "externalLink", dimension: CGFloat) -> NSTextAttachment {
let imageAttachment = NSTextAttachment()
imageAttachment.image = MVMCoreCache.shared()?.getImageFromRegisteredBundles(name)
imageAttachment.bounds = CGRect(x: 0, y: 0, width: dimension, height: dimension)
return imageAttachment
}
static func getTextAttachmentFrom(url: String, dimension: CGFloat, label: Label) -> NSTextAttachment {
let imageAttachment = NSTextAttachment()
imageAttachment.bounds = CGRect(x: 0, y: 0, width: dimension, height: dimension)
DispatchQueue.global(qos: .default).async {
MVMCoreCache.shared()?.getImage(url, useWidth: false, widthForS7: 0, useHeight: false, heightForS7: 0, localFallbackImageName: nil) { image, data, _ in
DispatchQueue.main.sync {
imageAttachment.image = image
label.setNeedsDisplay()
}
}
}
return imageAttachment
}
/// Call to detect in the attributedText contains an NSTextAttachment.
func containsTextAttachment() -> Bool {
guard let attributedText = attributedText else { return false }
var containsAttachment = false
attributedText.enumerateAttribute(.attachment, in: NSRange(location: 0, length: attributedText.length), options: []) { value, range, stop in
if value is NSTextAttachment {
containsAttachment = true
return
}
}
return containsAttachment
}
func appendActionableClause(range: NSRange, actionBlock: @escaping ActionBlock) {
accessibilityTraits = .button
let accessibleAction = customAccessibilityAction(range: range)
clauses.append(ActionableClause(range: range, actionBlock: actionBlock, accessibilityID: accessibleAction?.hash ?? -1))
}
// Mark: - Old Helpers
extension Label {
public static func boundingRect(forCharacterRange range: NSRange, in label: Label) -> CGRect {
@ -789,25 +383,11 @@ public typealias ActionBlock = () -> ()
return (textContainer, layoutManager, textStorage)
}
}
// MARK: - Atomization
extension Label {
public func reset() {
text = nil
attributedText = nil
hero = nil
textAlignment = .left
originalAttributedString = nil
styleB2(true)
accessibilityCustomActions = []
clauses = []
accessibilityTraits = .staticText
numberOfLines = 0
}
public func needsToBeConstrained() -> Bool { true }
public func horizontalAlignment() -> UIStackView.Alignment { .leading }
@ -817,19 +397,16 @@ extension Label {
// MARK: - Multi-Link Functionality
extension Label {
/// Applied to existing text. Removes underlines of tappable links and assoated actionable clauses.
@objc public func clearActionableClauses() {
guard let attributedText = attributedText else { return }
let mutableAttributedString = NSMutableAttributedString(attributedString: attributedText)
clauses.forEach { clause in
mutableAttributedString.removeAttribute(NSAttributedString.Key.underlineStyle, range: clause.range)
}
self.attributedText = mutableAttributedString
accessibilityElements = []
clauses = []
/// Underlines the tappable region and stores the tap logic for interation.
private func setTextLinkState(range: NSRange, actionBlock: @escaping ActionBlock) {
var textLink = ActionLabelAttribute(location: range.location, length: range.length)
textLink.subscriber = textLink
.action
.sink { _ in
actionBlock()
}
attributes?.append(textLink)
}
public func createActionBlockFor(actionMap: [AnyHashable: Any]?, additionalData: [AnyHashable: Any]?, delegateObject: DelegateObject?) -> ActionBlock? {
@ -842,24 +419,6 @@ extension Label {
}
}
private func addActionAttributes(range: NSRange, string: NSMutableAttributedString?) {
guard let string = string,
let range = validateAttribute(range: range, in: string)
else { return }
string.addAttributes([NSAttributedString.Key.underlineStyle: NSUnderlineStyle.single.rawValue], range: range)
}
fileprivate func setActionAttributes(range: NSRange) {
guard let attributedText = attributedText else { return }
let mutableAttributedString = NSMutableAttributedString(attributedString: attributedText)
addActionAttributes(range: range, string: mutableAttributedString)
self.attributedText = mutableAttributedString
}
/**
Provides an actionable range of text.
@ -898,113 +457,6 @@ extension Label {
setTextLinkState(range: getRange, actionBlock: actionBlock)
}
/// Underlines the tappable region and stores the tap logic for interation.
private func setTextLinkState(range: NSRange, actionBlock: @escaping ActionBlock) {
setActionAttributes(range: range)
appendActionableClause(range: range, actionBlock: actionBlock)
}
@objc private func textLinkTapped(_ gesture: UITapGestureRecognizer) {
for clause in clauses {
// This determines if we tapped on the desired range of text.
if gesture.didTapAttributedTextInLabel(self, inRange: clause.range) {
clause.performAction()
return
}
}
}
}
// MARK: -
extension UITapGestureRecognizer {
func didTapAttributedTextInLabel(_ label: Label, inRange targetRange: NSRange) -> Bool {
// There would only ever be one clause to act on.
if label.makeWholeViewClickable {
return true
}
guard let abstractContainer = label.abstractTextContainer() else { return false }
let textContainer = abstractContainer.0
let layoutManager = abstractContainer.1
let tapLocation = location(in: label)
let indexOfGlyph = layoutManager.glyphIndex(for: tapLocation, in: textContainer)
let intrinsicWidth = label.intrinsicContentSize.width
// Assert that tapped occured within acceptable bounds based on alignment.
switch label.textAlignment {
case .right:
if tapLocation.x < label.bounds.width - intrinsicWidth {
return false
}
case .center:
let halfBounds = label.bounds.width / 2
let halfIntrinsicWidth = intrinsicWidth / 2
if tapLocation.x > halfBounds + halfIntrinsicWidth {
return false
} else if tapLocation.x < halfBounds - halfIntrinsicWidth {
return false
}
default: // Left align
if tapLocation.x > intrinsicWidth {
return false
}
}
// Affirms that the tap occured in the desired rect of provided by the target range.
return layoutManager.boundingRect(forGlyphRange: targetRange, in: textContainer).contains(tapLocation) && NSLocationInRange(indexOfGlyph, targetRange)
}
}
// MARK: - Accessibility
extension Label {
func customAccessibilityAction(range: NSRange) -> UIAccessibilityCustomAction? {
guard let text = text else { return nil }
if accessibilityHint == nil {
accessibilityHint = MVMCoreUIUtility.hardcodedString(withKey: "swipe_to_select_with_action_hint")
}
let actionText = NSString(string: text).substring(with: range)
let accessibleAction = UIAccessibilityCustomAction(name: actionText, target: self, selector: #selector(accessibilityCustomAction(_:)))
accessibilityCustomActions?.append(accessibleAction)
return accessibleAction
}
@objc public func accessibilityCustomAction(_ action: UIAccessibilityCustomAction) {
for clause in clauses {
if action.hash == clause.accessibilityID {
clause.performAction()
return
}
}
}
open override func accessibilityActivate() -> Bool {
guard let accessibleActions = accessibilityCustomActions else { return false }
for clause in clauses {
for action in accessibleActions {
if action.hash == clause.accessibilityID {
clause.performAction()
return true
}
}
}
return false
}
}
//------------------------------------------------------
@ -1025,3 +477,4 @@ func validateAttribute(range: NSRange, in string: NSAttributedString, type: Stri
return range
}