From 5a22e6ecd0a8d5660b8c90611ef3b352bcdcb380 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 3 Aug 2022 17:03:59 -0500 Subject: [PATCH] first cut Signed-off-by: Matt Bruce --- VDS.xcodeproj/project.pbxproj | 34 +++- VDS/Components/Checkbox/CheckboxModel.swift | 10 +- .../Attributes/LabelAttributeAction.swift | 41 ++++ .../Attributes/LabelAttributeColor.swift | 31 +++ .../Label/Attributes/LabelAttributeFont.swift | 36 ++++ .../Attributes/LabelAttributeModel.swift | 21 ++ .../LabelAttributeStrikeThrough.swift | 24 +++ .../Attributes/LabelAttributeUnderline.swift | 91 +++++++++ VDS/Components/Label/Label.swift | 181 ++++++++++++++++-- VDS/Components/Label/LabelModel.swift | 10 +- VDS/Protocols/Fontable.swift | 14 ++ VDS/Protocols/Labelable.swift | 5 +- 12 files changed, 476 insertions(+), 22 deletions(-) create mode 100644 VDS/Components/Label/Attributes/LabelAttributeAction.swift create mode 100644 VDS/Components/Label/Attributes/LabelAttributeColor.swift create mode 100644 VDS/Components/Label/Attributes/LabelAttributeFont.swift create mode 100644 VDS/Components/Label/Attributes/LabelAttributeModel.swift create mode 100644 VDS/Components/Label/Attributes/LabelAttributeStrikeThrough.swift create mode 100644 VDS/Components/Label/Attributes/LabelAttributeUnderline.swift create mode 100644 VDS/Protocols/Fontable.swift diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index 94283153..ab288fd6 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -49,6 +49,12 @@ EAF7F0A2289AFB3900B287F5 /* Errorable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F0A1289AFB3900B287F5 /* Errorable.swift */; }; EAF7F0A4289B017C00B287F5 /* LabelAttributeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F0A3289B017C00B287F5 /* LabelAttributeModel.swift */; }; EAF7F0A6289B0CE000B287F5 /* Resetable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F0A5289B0CE000B287F5 /* Resetable.swift */; }; + EAF7F0A8289B119400B287F5 /* Fontable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F0A7289B119400B287F5 /* Fontable.swift */; }; + EAF7F0AB289B13FD00B287F5 /* LabelAttributeFont.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F0AA289B13FD00B287F5 /* LabelAttributeFont.swift */; }; + EAF7F0AD289B142900B287F5 /* LabelAttributeStrikeThrough.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F0AC289B142900B287F5 /* LabelAttributeStrikeThrough.swift */; }; + EAF7F0AF289B144C00B287F5 /* LabelAttributeUnderline.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F0AE289B144C00B287F5 /* LabelAttributeUnderline.swift */; }; + EAF7F0B1289B177F00B287F5 /* LabelAttributeColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F0B0289B177F00B287F5 /* LabelAttributeColor.swift */; }; + EAF7F0B3289B1ADC00B287F5 /* LabelAttributeAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F0B2289B1ADC00B287F5 /* LabelAttributeAction.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -105,6 +111,12 @@ EAF7F0A1289AFB3900B287F5 /* Errorable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Errorable.swift; sourceTree = ""; }; EAF7F0A3289B017C00B287F5 /* LabelAttributeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelAttributeModel.swift; sourceTree = ""; }; EAF7F0A5289B0CE000B287F5 /* Resetable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Resetable.swift; sourceTree = ""; }; + EAF7F0A7289B119400B287F5 /* Fontable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fontable.swift; sourceTree = ""; }; + EAF7F0AA289B13FD00B287F5 /* LabelAttributeFont.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelAttributeFont.swift; sourceTree = ""; }; + EAF7F0AC289B142900B287F5 /* LabelAttributeStrikeThrough.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelAttributeStrikeThrough.swift; sourceTree = ""; }; + EAF7F0AE289B144C00B287F5 /* LabelAttributeUnderline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelAttributeUnderline.swift; sourceTree = ""; }; + EAF7F0B0289B177F00B287F5 /* LabelAttributeColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelAttributeColor.swift; sourceTree = ""; }; + EAF7F0B2289B1ADC00B287F5 /* LabelAttributeAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelAttributeAction.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -220,6 +232,7 @@ EA3361AC288B26190071C351 /* DataTrackable.swift */, EA3361A9288B25E40071C351 /* Disabling.swift */, EAF7F0A1289AFB3900B287F5 /* Errorable.swift */, + EAF7F0A7289B119400B287F5 /* Fontable.swift */, EA3361AE288B26310071C351 /* FormFieldable.swift */, EA33624628931B050071C351 /* Initable.swift */, EA3362442892F9130071C351 /* Labelable.swift */, @@ -284,7 +297,7 @@ children = ( EA33623F2892EF6B0071C351 /* Label.swift */, EA3362422892EFF20071C351 /* LabelModel.swift */, - EAF7F0A3289B017C00B287F5 /* LabelAttributeModel.swift */, + EAF7F0A9289B13EF00B287F5 /* Attributes */, ); path = Label; sourceTree = ""; @@ -298,6 +311,19 @@ path = Checkbox; sourceTree = ""; }; + EAF7F0A9289B13EF00B287F5 /* Attributes */ = { + isa = PBXGroup; + children = ( + EAF7F0A3289B017C00B287F5 /* LabelAttributeModel.swift */, + EAF7F0AA289B13FD00B287F5 /* LabelAttributeFont.swift */, + EAF7F0AC289B142900B287F5 /* LabelAttributeStrikeThrough.swift */, + EAF7F0AE289B144C00B287F5 /* LabelAttributeUnderline.swift */, + EAF7F0B0289B177F00B287F5 /* LabelAttributeColor.swift */, + EAF7F0B2289B1ADC00B287F5 /* LabelAttributeAction.swift */, + ); + path = Attributes; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -416,7 +442,9 @@ EA3361C328902D960071C351 /* Toggle.swift in Sources */, EAF7F0A0289AB7EC00B287F5 /* View.swift in Sources */, EA3362402892EF6C0071C351 /* Label.swift in Sources */, + EAF7F0B3289B1ADC00B287F5 /* LabelAttributeAction.swift in Sources */, EA33622E2891EA3C0071C351 /* DispatchQueue+Once.swift in Sources */, + EAF7F0AF289B144C00B287F5 /* LabelAttributeUnderline.swift in Sources */, EA3361C5289030FC0071C351 /* Accessable.swift in Sources */, EA33622C2891E73B0071C351 /* FontProtocol.swift in Sources */, EAF7F0952899861000B287F5 /* Checkbox.swift in Sources */, @@ -425,13 +453,16 @@ EA3362432892EFF20071C351 /* LabelModel.swift in Sources */, EA33624728931B050071C351 /* Initable.swift in Sources */, EAF7F0A4289B017C00B287F5 /* LabelAttributeModel.swift in Sources */, + EAF7F0B1289B177F00B287F5 /* LabelAttributeColor.swift in Sources */, EA3361BD288B2C760071C351 /* TypeAlias.swift in Sources */, EAF7F09A2899B17200B287F5 /* CATransaction.swift in Sources */, EAF7F0A2289AFB3900B287F5 /* Errorable.swift in Sources */, EA3C3B4C2894823E000CA526 /* AnyProxy-PropertyWrapper.swift in Sources */, EA3361AF288B26310071C351 /* FormFieldable.swift in Sources */, + EAF7F0A8289B119400B287F5 /* Fontable.swift in Sources */, EAF7F09E289AAEC000B287F5 /* Constants.swift in Sources */, EA3361B3288B265D0071C351 /* Changable.swift in Sources */, + EAF7F0AB289B13FD00B287F5 /* LabelAttributeFont.swift in Sources */, EA336171288B19200071C351 /* VDS.docc in Sources */, EAF7F0962899861000B287F5 /* CheckboxModel.swift in Sources */, EA3361AA288B25E40071C351 /* Disabling.swift in Sources */, @@ -440,6 +471,7 @@ EA3361AD288B26190071C351 /* DataTrackable.swift in Sources */, EA33623E2892EE950071C351 /* UIDevice.swift in Sources */, EA3362302891EB4A0071C351 /* Fonts.swift in Sources */, + EAF7F0AD289B142900B287F5 /* LabelAttributeStrikeThrough.swift in Sources */, EA3361B8288B2AAA0071C351 /* ViewProtocol.swift in Sources */, EA3361BF288B2EA60071C351 /* ModelHandlerable.swift in Sources */, EA3361A8288B23300071C351 /* UIColor.swift in Sources */, diff --git a/VDS/Components/Checkbox/CheckboxModel.swift b/VDS/Components/Checkbox/CheckboxModel.swift index 80845fa5..3c8644fa 100644 --- a/VDS/Components/Checkbox/CheckboxModel.swift +++ b/VDS/Components/Checkbox/CheckboxModel.swift @@ -12,11 +12,13 @@ public protocol CheckboxModel: FormFieldable, Errorable, DataTrackable, Accessab var id: String? { get set } var on: Bool { get set } var labelText: String? { get set } + var labelTextAttributes: [LabelAttributeModel]? { get set } var childText: String? { get set } + var childTextAttributes: [LabelAttributeModel]? { get set } } extension CheckboxModel { - public var fontCategory: VDSFontCategory { + public var fontCategory: FontCategory { get { return .body } set { return } } @@ -41,6 +43,7 @@ extension CheckboxModel { model.text = labelText model.surface = surface model.disabled = disabled + model.attributes = labelTextAttributes return model } @@ -54,6 +57,7 @@ extension CheckboxModel { model.text = childText model.surface = surface model.disabled = disabled + model.attributes = childTextAttributes return model } @@ -76,8 +80,10 @@ public struct DefaultCheckboxModel: CheckboxModel { public var on: Bool = false public var labelText: String? + public var labelTextAttributes: [LabelAttributeModel]? public var childText: String? - + public var childTextAttributes: [LabelAttributeModel]? + public var showError: Bool = false public var errorText: String? diff --git a/VDS/Components/Label/Attributes/LabelAttributeAction.swift b/VDS/Components/Label/Attributes/LabelAttributeAction.swift new file mode 100644 index 00000000..da3e19fa --- /dev/null +++ b/VDS/Components/Label/Attributes/LabelAttributeAction.swift @@ -0,0 +1,41 @@ +// +// LabelAttributeAction.swift +// VDS +// +// Created by Matt Bruce on 8/3/22. +// + +import Foundation +import UIKit + +public protocol LabelAttributeActionable: LabelAttributeModel { + var action: Blocks.ActionBlock { get set } +} + +public struct LabelAttributeActionModel: LabelAttributeActionable { + + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + public var location: Int + public var length: Int + public var action: Blocks.ActionBlock = {} + + //-------------------------------------------------- + // MARK: - Initializer + //-------------------------------------------------- + + public init(location: Int, length: Int, action: @escaping Blocks.ActionBlock) { + self.location = location + self.length = length + self.action = action + } + + private enum CodingKeys: String, CodingKey { + case location, length + } + + public func setAttribute(on attributedString: NSMutableAttributedString) { + attributedString.addAttribute(.underlineStyle, value: UnderlineStyle.single.value(), range: range) + } +} diff --git a/VDS/Components/Label/Attributes/LabelAttributeColor.swift b/VDS/Components/Label/Attributes/LabelAttributeColor.swift new file mode 100644 index 00000000..59f9727f --- /dev/null +++ b/VDS/Components/Label/Attributes/LabelAttributeColor.swift @@ -0,0 +1,31 @@ +// +// LabelAttributeColor.swift +// VDS +// +// Created by Matt Bruce on 8/3/22. +// + +import Foundation +import UIKit + +public struct LabelAttributeColor: LabelAttributeModel { + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + public var location: Int + public var length: Int + public var color: String + //-------------------------------------------------- + // MARK: - Initializer + //-------------------------------------------------- + public init(location: Int, length: Int, color: String = "#000000") { + self.location = location + self.length = length + self.color = color + } + + public func setAttribute(on attributedString: NSMutableAttributedString) { + attributedString.removeAttribute(.foregroundColor, range: range) + attributedString.addAttribute(.foregroundColor, value: UIColor(hexString: color), range: range) + } +} diff --git a/VDS/Components/Label/Attributes/LabelAttributeFont.swift b/VDS/Components/Label/Attributes/LabelAttributeFont.swift new file mode 100644 index 00000000..f0575c63 --- /dev/null +++ b/VDS/Components/Label/Attributes/LabelAttributeFont.swift @@ -0,0 +1,36 @@ +// +// LabelAttributeFont.swift +// VDS +// +// Created by Matt Bruce on 8/3/22. +// + +import Foundation +import UIKit + +public struct LabelAttributeFont: LabelAttributeModel { + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + public var location: Int + public var length: Int + public var style: FontStyle + public var color: String + //-------------------------------------------------- + // MARK: - Initializer + //-------------------------------------------------- + public init(location: Int, length: Int, style: FontStyle, color: String = "#000000") { + self.location = location + self.length = length + self.style = style + self.color = color + } + + public func setAttribute(on attributedString: NSMutableAttributedString) { + + attributedString.removeAttribute(.font, range: range) + attributedString.removeAttribute(.foregroundColor, range: range) + attributedString.addAttribute(.font, value: style.font, range: range) + attributedString.addAttribute(.foregroundColor, value: UIColor(hexString: color), range: range) + } +} diff --git a/VDS/Components/Label/Attributes/LabelAttributeModel.swift b/VDS/Components/Label/Attributes/LabelAttributeModel.swift new file mode 100644 index 00000000..0341b477 --- /dev/null +++ b/VDS/Components/Label/Attributes/LabelAttributeModel.swift @@ -0,0 +1,21 @@ +// +// LabelAttributeModel.swift +// VDS +// +// Created by Matt Bruce on 8/3/22. +// + +import Foundation +import UIKit + +public protocol LabelAttributeModel: Codable { + var location: Int { get set } + var length: Int { get set } + func setAttribute(on attributedString: NSMutableAttributedString) +} + +extension LabelAttributeModel { + public var range: NSRange { + NSRange(location: location, length: length) + } +} diff --git a/VDS/Components/Label/Attributes/LabelAttributeStrikeThrough.swift b/VDS/Components/Label/Attributes/LabelAttributeStrikeThrough.swift new file mode 100644 index 00000000..29829af0 --- /dev/null +++ b/VDS/Components/Label/Attributes/LabelAttributeStrikeThrough.swift @@ -0,0 +1,24 @@ +// +// LabelAttributeStrikeThrough.swift +// VDS +// +// Created by Matt Bruce on 8/3/22. +// + +import Foundation +import UIKit + +public struct LabelAttributeStrikeThrough: LabelAttributeModel { + public var location: Int + public var length: Int + + public init(location: Int, length: Int) { + self.location = location + self.length = length + } + + public func setAttribute(on attributedString: NSMutableAttributedString) { + attributedString.addAttribute(.strikethroughStyle, value: NSUnderlineStyle.thick.rawValue, range: range) + attributedString.addAttribute(.baselineOffset, value: 0, range: range) + } +} diff --git a/VDS/Components/Label/Attributes/LabelAttributeUnderline.swift b/VDS/Components/Label/Attributes/LabelAttributeUnderline.swift new file mode 100644 index 00000000..f904d52b --- /dev/null +++ b/VDS/Components/Label/Attributes/LabelAttributeUnderline.swift @@ -0,0 +1,91 @@ +// +// LabelAttributeUnderline.swift +// VDS +// +// Created by Matt Bruce on 8/3/22. +// + +import Foundation +import UIKit + +public struct LabelAttributeUnderline: LabelAttributeModel { + public var location: Int + public var length: Int + public var color: String? + public var style: UnderlineStyle = .single + public var pattern: UnderlineStyle.Pattern? + + public var underlineValue: NSUnderlineStyle { + if let pattern = pattern?.value() { + return NSUnderlineStyle(rawValue: style.value() | pattern) + } else { + return NSUnderlineStyle(rawValue: style.value()) + } + } + + public init(location: Int, length: Int, style: UnderlineStyle = .single, color: String? = nil, pattern: UnderlineStyle.Pattern? = nil) { + self.location = location + self.length = length + self.color = color + self.style = style + self.pattern = pattern + } + + public func setAttribute(on attributedString: NSMutableAttributedString) { + attributedString.addAttribute(.underlineStyle, value: underlineValue.rawValue, range: range) + if let color = color { + attributedString.addAttribute(.underlineColor, value: UIColor(hexString: color), range: range) + } + } +} + +public enum UnderlineStyle: String, Codable { + case none + case single + case thick + case double + + func value() -> Int { + switch self { + case .none: + return 0 + + case .single: + return NSUnderlineStyle.single.rawValue + + case .thick: + return NSUnderlineStyle.thick.rawValue + + case .double: + return NSUnderlineStyle.double.rawValue + } + } + + public enum Pattern: String, Codable { + case dot + case dash + case dashDot + case dashDotDot + case byWord + + func value() -> Int { + switch self { + case .dot: + return NSUnderlineStyle.patternDot.rawValue + + case .dash: + return NSUnderlineStyle.patternDash.rawValue + + case .dashDot: + return NSUnderlineStyle.patternDashDot.rawValue + + case .dashDotDot: + return NSUnderlineStyle.patternDashDotDot.rawValue + + case .byWord: + return NSUnderlineStyle.byWord.rawValue + } + } + } +} + diff --git a/VDS/Components/Label/Label.swift b/VDS/Components/Label/Label.swift index b29bc484..398c9dc4 100644 --- a/VDS/Components/Label/Label.swift +++ b/VDS/Components/Label/Label.swift @@ -10,22 +10,22 @@ import UIKit import VDSColorTokens import Combine -open class Label: UILabel, ModelHandlerable, Initable { +open class Label: UILabel, ModelHandlerable, Initable, Resettable { @Published public var model: LabelModel = DefaultLabelModel() private var cancellable: AnyCancellable? @Proxy(\.model.fontSize) - public var fontSize: VDSFontSize + public var fontSize: FontSize @Proxy(\.model.textPosition) - public var textPosition: VDSTextPosition + public var textPosition: TextPosition @Proxy(\.model.fontWeight) - public var fontWeight: VDSFontWeight + public var fontWeight: FontWeight @Proxy(\.model.fontCategory) - public var fontCategory: VDSFontCategory + public var fontCategory: FontCategory @Proxy(\.model.surface) public var surface: Surface @@ -51,12 +51,29 @@ open class Label: UILabel, ModelHandlerable, Initable { setup() } - private func setup() { + open func setup() { + backgroundColor = .clear + numberOfLines = 0 + lineBreakMode = .byWordWrapping + translatesAutoresizingMaskIntoConstraints = false + accessibilityCustomActions = [] + accessibilityTraits = .staticText cancellable = $model.debounce(for: .seconds(Constants.ModelStateDebounce), scheduler: RunLoop.main).sink { [weak self] viewModel in self?.onStateChange(viewModel: viewModel) } } + public func reset() { + text = nil + attributedText = nil + textColor = .black + font = FontStyle.RegularBodyLarge.font + textAlignment = .left + accessibilityCustomActions = [] + accessibilityTraits = .staticText + numberOfLines = 0 + } + private func getTextColor(for disabled: Bool, surface: Surface) -> UIColor { if disabled { if surface == .light { @@ -75,19 +92,161 @@ open class Label: UILabel, ModelHandlerable, Initable { //functions private func onStateChange(viewModel: LabelModel) { - text = viewModel.text textAlignment = viewModel.textPosition.textAlignment textColor = getTextColor(for: viewModel.disabled, surface: viewModel.surface) - guard let vdsFont = try? VDSFontStyle.font(for: viewModel.fontCategory, fontWeight: viewModel.fontWeight, fontSize: viewModel.fontSize) else { - font = VDSFontStyle.RegularBodyLarge.font - return + if let vdsFont = try? FontStyle.font(for: viewModel.fontCategory, fontWeight: viewModel.fontWeight, fontSize: viewModel.fontSize) { + font = vdsFont + } else { + font = FontStyle.RegularBodyLarge.font + } + + if let attributes = viewModel.attributes, let text = model.text, let font = font, let textColor = textColor { + let startingAttributes = [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: textColor] + let mutableText = NSMutableAttributedString(string: text, attributes: startingAttributes) + var hasActionable = false + for attribute in attributes { + attribute.setAttribute(on: mutableText) + if let attributeActionable = attribute as? LabelAttributeActionable { + hasActionable = true + setTextLinkState(range: attributeActionable.range) { + attributeActionable.action() + } + } + } + + if hasActionable { + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(textLinkTapped)) + tapGesture.numberOfTapsRequired = 1 + addGestureRecognizer(tapGesture) + } + + attributedText = mutableText + } else { + text = viewModel.text + } + + } + + //------------------------------------------------------ + // 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 + } + } } - font = vdsFont } + /// Used for tappable links in the text. + public struct ActionableClause { + public var range: NSRange + public var actionBlock: Blocks.ActionBlock + public var accessibilityID: Int = 0 + + public func performAction() { + actionBlock() + } + + public init(range: NSRange, actionBlock: @escaping Blocks.ActionBlock, accessibilityID: Int = 0) { + self.range = range + self.actionBlock = actionBlock + self.accessibilityID = accessibilityID + } + } + + private func setTextLinkState(range: NSRange, actionBlock: @escaping Blocks.ActionBlock) { + clauses.append(ActionableClause(range: range, actionBlock: actionBlock, accessibilityID: -1)) + } + + @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 + } + } + } + + /** + Provides a text container and layout manager of how the text would appear on screen. + They are used in tandem to derive low-level TextKit results of the label. + */ + public func abstractTextContainer() -> (NSTextContainer, NSLayoutManager, NSTextStorage)? { + + // Must configure the attributed string to translate what would appear on screen to accurately analyze. + guard let attributedText = attributedText else { return nil } + + let paragraph = NSMutableParagraphStyle() + paragraph.alignment = textAlignment + + let stagedAttributedString = NSMutableAttributedString(attributedString: attributedText) + stagedAttributedString.addAttributes([NSAttributedString.Key.paragraphStyle: paragraph], range: NSRange(location: 0, length: attributedText.string.count)) + + let textStorage = NSTextStorage(attributedString: stagedAttributedString) + let layoutManager = NSLayoutManager() + let textContainer = NSTextContainer(size: .zero) + + layoutManager.addTextContainer(textContainer) + textStorage.addLayoutManager(layoutManager) + + textContainer.lineFragmentPadding = 0.0 + textContainer.lineBreakMode = lineBreakMode + textContainer.maximumNumberOfLines = numberOfLines + textContainer.size = bounds.size + + return (textContainer, layoutManager, textStorage) + } + //Modelable public func set(with model: LabelModel) { self.model = model } } + +// MARK: - +extension UITapGestureRecognizer { + + func didTapAttributedTextInLabel(_ label: Label, inRange targetRange: NSRange) -> Bool { + + 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) + } +} diff --git a/VDS/Components/Label/LabelModel.swift b/VDS/Components/Label/LabelModel.swift index e6527435..bb9c5f36 100644 --- a/VDS/Components/Label/LabelModel.swift +++ b/VDS/Components/Label/LabelModel.swift @@ -10,14 +10,16 @@ import UIKit public protocol LabelModel: Labelable, Surfaceable, Disabling { var text: String? { get set } + var attributes: [LabelAttributeModel]? { get set } } open class DefaultLabelModel: LabelModel { public var text: String? - public var fontCategory: VDSFontCategory = .body - public var fontSize: VDSFontSize = .small - public var fontWeight: VDSFontWeight = .regular - public var textPosition: VDSTextPosition = .left + public var attributes: [LabelAttributeModel]? + public var fontCategory: FontCategory = .body + public var fontSize: FontSize = .small + public var fontWeight: FontWeight = .regular + public var textPosition: TextPosition = .left public var surface: Surface = .light public var disabled: Bool = false required public init(){} diff --git a/VDS/Protocols/Fontable.swift b/VDS/Protocols/Fontable.swift new file mode 100644 index 00000000..a2b6beb3 --- /dev/null +++ b/VDS/Protocols/Fontable.swift @@ -0,0 +1,14 @@ +// +// Fontable.swift +// VDS +// +// Created by Matt Bruce on 8/3/22. +// + +import Foundation + +public protocol Fontable { + var fontSize: FontSize { get set } + var fontWeight: FontWeight { get set } + var fontCategory: FontCategory { get set } +} diff --git a/VDS/Protocols/Labelable.swift b/VDS/Protocols/Labelable.swift index 4ef59b8f..26c2eef2 100644 --- a/VDS/Protocols/Labelable.swift +++ b/VDS/Protocols/Labelable.swift @@ -7,9 +7,6 @@ import Foundation -public protocol Labelable { - var fontSize: FontSize { get set } +public protocol Labelable: Fontable { var textPosition: TextPosition { get set } - var fontWeight: FontWeight { get set } - var fontCategory: FontCategory { get set } }