refactored label for first cut, pretty big changes though....
Signed-off-by: Matt Bruce <matt.bruce@verizon.com>
This commit is contained in:
parent
29f43b7861
commit
ddb15e72f3
@ -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
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user