From ddb15e72f3f9d846e0f43910aefc79c865f75ca2 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Mon, 12 Feb 2024 17:32:06 -0600 Subject: [PATCH] refactored label for first cut, pretty big changes though.... Signed-off-by: Matt Bruce --- .../Atomic/Atoms/Views/Label/Label.swift | 781 +++--------------- 1 file changed, 117 insertions(+), 664 deletions(-) diff --git a/MVMCoreUI/Atomic/Atoms/Views/Label/Label.swift b/MVMCoreUI/Atomic/Atoms/Views/Label/Label.swift index 59611756..609c5e0f 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Label/Label.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Label/Label.swift @@ -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 } +