Merge branch 'develop' into feature/label_accessibility

# Conflicts:
#	MVMCoreUI/Atoms/Views/Label.swift
This commit is contained in:
Kevin G Christiano 2019-09-04 14:08:10 -04:00
commit a6e732afa9

View File

@ -9,16 +9,15 @@
import MVMCore import MVMCore
public typealias ActionBlock = () -> Void public typealias ActionBlock = () -> ()
@objcMembers open class Label: UILabel, MVMCoreViewProtocol, MVMCoreUIMoleculeViewProtocol, MVMCoreUIViewConstrainingProtocol, MFButtonProtocol { @objcMembers open class Label: UILabel, MVMCoreViewProtocol, MVMCoreUIMoleculeViewProtocol, MVMCoreUIViewConstrainingProtocol, MFButtonProtocol {
//------------------------------------------------------ //------------------------------------------------------
// MARK: - Properties // MARK: - Properties
//------------------------------------------------------ //------------------------------------------------------
public var makeWholeViewClickable = false public var makeWholeViewClickable = false
fileprivate var hasAttachmentImage = false
/// Set this property if you want updateView to update the font based on this standard and the size passed in. /// Set this property if you want updateView to update the font based on this standard and the size passed in.
public var standardFontSize: CGFloat = 0.0 public var standardFontSize: CGFloat = 0.0
@ -40,14 +39,23 @@ public typealias ActionBlock = () -> Void
//------------------------------------------------------ //------------------------------------------------------
public var clauses: [ActionableClause] = [] { public var clauses: [ActionableClause] = [] {
didSet { isUserInteractionEnabled = !clauses.isEmpty } didSet {
isUserInteractionEnabled = !clauses.isEmpty
if clauses.count > 1 {
clauses.sort { first, second in
guard let firstLocation = first.range?.location,
let secondLocation = second.range?.location
else { return false }
return firstLocation < secondLocation
}
}
}
} }
/// Used for tappable links in the text. /// Used for tappable links in the text.
public struct ActionableClause { public struct ActionableClause {
var range: NSRange? var range: NSRange?
var actionBlock: ActionBlock? var actionBlock: ActionBlock?
// var text: String?
var hash: Int = 0 var hash: Int = 0
func performAction() { func performAction() {
@ -66,6 +74,10 @@ public typealias ActionBlock = () -> Void
lineBreakMode = .byWordWrapping lineBreakMode = .byWordWrapping
translatesAutoresizingMaskIntoConstraints = false translatesAutoresizingMaskIntoConstraints = false
accessibilityCustomActions = [] accessibilityCustomActions = []
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(textLinkTapped(_:)))
tapGesture.numberOfTapsRequired = 1
addGestureRecognizer(tapGesture)
} }
@objc public init() { @objc public init() {
@ -113,6 +125,7 @@ public typealias ActionBlock = () -> Void
return label return label
} }
/// H32 -> Head
@objc public static func commonLabelH32(_ scale: Bool) -> Label { @objc public static func commonLabelH32(_ scale: Bool) -> Label {
let label = Label.label() let label = Label.label()
label.styleH32(scale) label.styleH32(scale)
@ -140,12 +153,14 @@ public typealias ActionBlock = () -> Void
return label return label
} }
/// B20 -> Body
@objc public static func commonLabelB20(_ scale: Bool) -> Label { @objc public static func commonLabelB20(_ scale: Bool) -> Label {
let label = Label.label() let label = Label.label()
label.styleB20(scale) label.styleB20(scale)
return label return label
} }
/// Default
@objc open class func label() -> Label { @objc open class func label() -> Label {
return Label(frame: .zero) return Label(frame: .zero)
} }
@ -189,6 +204,10 @@ public typealias ActionBlock = () -> Void
} }
} }
if let wholeViewIsClickable = json?.boolForKey("makeWholeViewClickable") {
(label as? Label)?.makeWholeViewClickable = wholeViewIsClickable
}
if let backgroundColorHex = json?.optionalStringForKey(KeyBackgroundColor), !backgroundColorHex.isEmpty { if let backgroundColorHex = json?.optionalStringForKey(KeyBackgroundColor), !backgroundColorHex.isEmpty {
label.backgroundColor = UIColor.mfGet(forHex: backgroundColorHex) label.backgroundColor = UIColor.mfGet(forHex: backgroundColorHex)
} }
@ -229,21 +248,27 @@ public typealias ActionBlock = () -> Void
case "strikethrough": case "strikethrough":
attributedString.addAttribute(.strikethroughStyle, value: NSUnderlineStyle.thick.rawValue, range: range) attributedString.addAttribute(.strikethroughStyle, value: NSUnderlineStyle.thick.rawValue, range: range)
attributedString.addAttribute(.baselineOffset, value: 0, range: range) attributedString.addAttribute(.baselineOffset, value: 0, range: range)
case "color": case "color":
if let colorHex = attribute.optionalStringForKey(KeyTextColor), !colorHex.isEmpty { if let colorHex = attribute.optionalStringForKey(KeyTextColor), !colorHex.isEmpty {
attributedString.removeAttribute(.foregroundColor, range: range) attributedString.removeAttribute(.foregroundColor, range: range)
attributedString.addAttribute(.foregroundColor, value: UIColor.mfGet(forHex: colorHex), range: range) attributedString.addAttribute(.foregroundColor, value: UIColor.mfGet(forHex: colorHex), range: range)
} }
case "externalLink": case "image":
let fontSize = attribute["size"] as? CGFloat ?? label.font.pointSize let fontSize = attribute["size"] as? CGFloat ?? label.font.pointSize
let imageAttachment = Label.getTextAttachmentImage(dimension: fontSize) 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() let mutableString = NSMutableAttributedString()
mutableString.append(NSAttributedString(string: " "))
mutableString.append(NSAttributedString(attachment: imageAttachment)) mutableString.append(NSAttributedString(attachment: imageAttachment))
attributedString.insert(mutableString, at: location) attributedString.insert(mutableString, at: location)
(label as? Label)?.hasAttachmentImage = true
case "font": case "font":
if let fontStyle = attribute.optionalStringForKey("style") { if let fontStyle = attribute.optionalStringForKey("style") {
@ -254,13 +279,13 @@ public typealias ActionBlock = () -> Void
} else { } else {
let fontSize = attribute["size"] as? CGFloat let fontSize = attribute["size"] as? CGFloat
var font: UIFont? var font: UIFont?
if let fontName = attribute.optionalStringForKey("name") { if let fontName = attribute.optionalStringForKey("name") {
font = MFFonts.mfFont(withName: fontName, size: fontSize ?? label.font.pointSize) font = MFFonts.mfFont(withName: fontName, size: fontSize ?? label.font.pointSize)
} else if let fontSize = fontSize { } else if let fontSize = fontSize {
font = label.font.withSize(fontSize) font = label.font.withSize(fontSize)
} }
if let font = font { if let font = font {
attributedString.removeAttribute(.font, range: range) attributedString.removeAttribute(.font, range: range)
attributedString.addAttribute(.font, value: font, range: range) attributedString.addAttribute(.font, value: font, range: range)
@ -272,9 +297,8 @@ public typealias ActionBlock = () -> Void
actionLabel.addActionAttributes(range: range, string: attributedString) actionLabel.addActionAttributes(range: range, string: attributedString)
let accessibleAction = actionLabel.customAccessibilityAction(range: range) let accessibleAction = actionLabel.customAccessibilityAction(range: range)
let actionBlock = actionLabel.createActionBlockFrom(actionMap: json, additionalData: additionalData, delegateObject: delegate) let actionBlock = actionLabel.createActionBlockFrom(actionMap: json, additionalData: additionalData, delegateObject: delegate)
let actionableClause = ActionableClause(range: range, actionBlock: actionBlock, hash: accessibleAction?.hash ?? -1) actionLabel.clauses.append(ActionableClause(range: range, actionBlock: actionBlock, hash: accessibleAction?.hash ?? -1))
actionLabel.clauses.append(actionableClause)
default: default:
continue continue
} }
@ -283,6 +307,10 @@ public typealias ActionBlock = () -> Void
} }
} }
//------------------------------------------------------
// MARK: - Methods
//------------------------------------------------------
@objc public func styleH1(_ scale: Bool) { @objc public func styleH1(_ scale: Bool) {
MFStyler.styleLabelH1(self, genericScaling: false) MFStyler.styleLabelH1(self, genericScaling: false)
setScale(scale) setScale(scale)
@ -329,15 +357,28 @@ public typealias ActionBlock = () -> Void
if let originalAttributedString = originalAttributedString { if let originalAttributedString = originalAttributedString {
let attributedString = NSMutableAttributedString(attributedString: originalAttributedString) let attributedString = NSMutableAttributedString(attributedString: originalAttributedString)
attributedString.removeAttribute(.font, range: NSRange(location: 0, length: attributedString.length)) attributedString.removeAttribute(.font, range: NSRange(location: 0, length: attributedString.length))
originalAttributedString.enumerateAttribute(.font, in: NSRange(location: 0, length: originalAttributedString.length), options: [], using: { value, range, stop in
// Loop the original attributed string, resize the fonts. // 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) { if let fontObj = value as? UIFont, let stylerSize = MFStyler.sizeObjectGeneric(forCurrentDevice: fontObj.pointSize)?.getValueBased(onSize: size) {
attributedString.addAttribute(.font, value: fontObj.withSize(stylerSize) as Any, range: range) attributedString.addAttribute(.font, value: fontObj.withSize(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 attributedText = attributedString
} else if !MVMCoreGetterUtility.fequal(a: Float(standardFontSize), b: 0.0), let sizeObject: MFSizeObject = self.sizeObject ?? MFStyler.sizeObjectGeneric(forCurrentDevice: standardFontSize) { } else if !MVMCoreGetterUtility.fequal(a: Float(standardFontSize), b: 0.0), let sizeObject = sizeObject ?? MFStyler.sizeObjectGeneric(forCurrentDevice: standardFontSize) {
self.font = self.font.withSize(sizeObject.getValueBased(onSize: size)) font = font.withSize(sizeObject.getValueBased(onSize: size))
} }
} }
@ -360,10 +401,10 @@ public typealias ActionBlock = () -> Void
} }
/** /**
Appends an external link image to the end of the attributed string. Appends an external link image to the end of the attributed string.
Will provide one whitespace to the left of the icon Will provide one whitespace to the left of the icon
*/ */
public func appendExternalLinkIcon() { @objc public func appendExternalLinkIcon() {
guard let attributedText = attributedText else { return } guard let attributedText = attributedText else { return }
@ -372,43 +413,80 @@ public typealias ActionBlock = () -> Void
mutableString.append(NSAttributedString(string: " ")) mutableString.append(NSAttributedString(string: " "))
mutableString.append(NSAttributedString(attachment: Label.getTextAttachmentImage(dimension: font.pointSize))) mutableString.append(NSAttributedString(attachment: Label.getTextAttachmentImage(dimension: font.pointSize)))
self.attributedText = mutableString self.attributedText = mutableString
hasAttachmentImage = true
} }
/** /**
Insert external link icon anywhere within text of Label. Insert external link icon anywhere within text of Label.
- Parameters: - Note: Each icon insertion adds 1 additional characters to the overall text length.
- index: Location within the associated text to insert an external Link Icon Therefore, you MUST insert icons and links in the order they would appear.
- Note: You will need to increment your current index by 2 for any changes to the text after inserting an icon. - parameter index: Location within the associated text to insert an external Link Icon
Each icon insertion adds 2 additional characters to the overall text length.
*/ */
public func insertExternalLinkIcon(at index: Int) { public func insertExternalLinkIcon(at index: Int) {
guard let attributedText = attributedText, index <= attributedText.string.count && index >= 0 else { return } guard let attributedText = attributedText, index <= attributedText.string.count && index >= 0 else { return }
let mutableString = NSMutableAttributedString(attributedString: attributedText) let mutableString = NSMutableAttributedString(attributedString: attributedText)
if index != 0 {
mutableString.insert(NSAttributedString(string: " "), at: index - 1)
}
mutableString.insert(NSAttributedString(attachment: Label.getTextAttachmentImage(dimension: font.pointSize)), at: index) mutableString.insert(NSAttributedString(attachment: Label.getTextAttachmentImage(dimension: font.pointSize)), at: index)
self.attributedText = mutableString
hasAttachmentImage = true self.attributedText = mutableString
} }
static func getTextAttachmentImage(dimension: CGFloat) -> NSTextAttachment { /*
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 dimension = round(dimension * 0.8) let dimension = round(dimension * 0.8)
let imageAttachment = NSTextAttachment() let imageAttachment = NSTextAttachment()
imageAttachment.image = MVMCoreUIUtility.imageNamed("externalLink") imageAttachment.image = MVMCoreUIUtility.imageNamed(name)
imageAttachment.bounds = CGRect(x: 0, y: 0, width: dimension, height: dimension) imageAttachment.bounds = CGRect(x: 0, y: 0, width: dimension, height: dimension)
return imageAttachment return imageAttachment
} }
static func getTextAttachmentFrom(url: String, dimension: CGFloat, label: Label) -> NSTextAttachment {
let dimension = round(dimension * 0.8)
let imageAttachment = NSTextAttachment()
imageAttachment.bounds = CGRect(x: 0, y: 0, width: dimension, height: dimension)
DispatchQueue.global(qos: .default).async {
guard let url = URL(string: url),
let data = try? Data(contentsOf: url)
else { return }
DispatchQueue.main.sync {
imageAttachment.image = UIImage(data: data)
label.setNeedsDisplay()
}
}
return imageAttachment
}
/// Call to detect in the attributedText contains an NSTextAttachment.
func textContainsTextAttachment() -> 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
}
} }
// MARK: - Atomization // MARK: - Atomization
@ -419,7 +497,6 @@ extension Label {
attributedText = nil attributedText = nil
textAlignment = .left textAlignment = .left
originalAttributedString = nil originalAttributedString = nil
hasAttachmentImage = false
styleB2(true) styleB2(true)
accessibilityCustomActions = [] accessibilityCustomActions = []
} }
@ -493,9 +570,8 @@ extension Label {
Provides an actionable range of text. Provides an actionable range of text.
- Attention: This method expects text to be set first. Otherwise, it will do nothing. - Attention: This method expects text to be set first. Otherwise, it will do nothing.
- Parameters: - parameter range: The range of text to be tapped.
- range: The range of text to be tapped. - parameter actionBlock: The code triggered when tapping the range of text.
- actionBlock: The code triggered when tapping the range of text.
*/ */
@objc public func addTappableLinkAttribute(range: NSRange, actionBlock: @escaping ActionBlock) { @objc public func addTappableLinkAttribute(range: NSRange, actionBlock: @escaping ActionBlock) {
@ -508,11 +584,10 @@ extension Label {
Provides an actionable range of text. Provides an actionable range of text.
- Attention: This method expects text to be set first. Otherwise, it will do nothing. - Attention: This method expects text to be set first. Otherwise, it will do nothing.
- Parameters: - parameter range: The range of text to be tapped.
- range: The range of text to be tapped. - parameter actionMap:
- actionMap: - parameter delegate:
- delegate: - parameter additionalData:
- additionalData:
*/ */
@objc public func addTappableLinkAttribute(range: NSRange, actionMap: [AnyHashable: Any]?, additionalData: [AnyHashable: Any]?, delegateObject: DelegateObject?) { @objc public func addTappableLinkAttribute(range: NSRange, actionMap: [AnyHashable: Any]?, additionalData: [AnyHashable: Any]?, delegateObject: DelegateObject?) {
@ -525,6 +600,8 @@ extension Label {
@objc private func textLinkTapped(_ gesture: UITapGestureRecognizer) { @objc private func textLinkTapped(_ gesture: UITapGestureRecognizer) {
for clause in clauses { for clause in clauses {
// This determines if we tapped on the desired range of text.
if let range = clause.range, gesture.didTapAttributedTextInLabel(self, inRange: range) { if let range = clause.range, gesture.didTapAttributedTextInLabel(self, inRange: range) {
clause.performAction() clause.performAction()
return return
@ -538,29 +615,23 @@ extension UITapGestureRecognizer {
func didTapAttributedTextInLabel(_ label: Label, inRange targetRange: NSRange) -> Bool { func didTapAttributedTextInLabel(_ label: Label, inRange targetRange: NSRange) -> Bool {
// There would only ever be one clause to act on.
if label.makeWholeViewClickable { if label.makeWholeViewClickable {
return true return true
} }
guard let attributedText = label.attributedText, let font = label.font else { return false } // Must configure the attributed string to translate what would appear on screen to accurately analyze.
guard let attributedText = label.attributedText else { return false }
var fontToAnalyze: UIFont? = font let paragraph = NSMutableParagraphStyle()
paragraph.alignment = label.textAlignment
// Necessary where label text is not consistent in font. let stagedAttributedString = NSMutableAttributedString(attributedString: attributedText)
for attr in attributedText.attributes(at: targetRange.location, effectiveRange: nil) { stagedAttributedString.addAttributes([NSAttributedString.Key.paragraphStyle: paragraph], range: NSRange(location: 0, length: attributedText.string.count))
if attr.key == NSAttributedString.Key.font {
fontToAnalyze = attr.value as? UIFont
}
}
let range = label.hasAttachmentImage ? NSRange(location: 0, length: attributedText.length) : targetRange
let mutableAttribString = NSMutableAttributedString(attributedString: attributedText)
mutableAttribString.addAttributes([NSAttributedString.Key.font: fontToAnalyze as Any], range: range)
let textStorage = NSTextStorage(attributedString: stagedAttributedString)
let layoutManager = NSLayoutManager() let layoutManager = NSLayoutManager()
let textContainer = NSTextContainer(size: .zero) let textContainer = NSTextContainer(size: .zero)
let textStorage = NSTextStorage(attributedString: mutableAttribString)
layoutManager.addTextContainer(textContainer) layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager) textStorage.addLayoutManager(layoutManager)