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
public typealias ActionBlock = () -> Void
public typealias ActionBlock = () -> ()
@objcMembers open class Label: UILabel, MVMCoreViewProtocol, MVMCoreUIMoleculeViewProtocol, MVMCoreUIViewConstrainingProtocol, MFButtonProtocol {
//------------------------------------------------------
// MARK: - Properties
//------------------------------------------------------
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.
public var standardFontSize: CGFloat = 0.0
@ -40,14 +39,23 @@ public typealias ActionBlock = () -> Void
//------------------------------------------------------
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.
public struct ActionableClause {
var range: NSRange?
var actionBlock: ActionBlock?
// var text: String?
var hash: Int = 0
func performAction() {
@ -66,6 +74,10 @@ public typealias ActionBlock = () -> Void
lineBreakMode = .byWordWrapping
translatesAutoresizingMaskIntoConstraints = false
accessibilityCustomActions = []
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(textLinkTapped(_:)))
tapGesture.numberOfTapsRequired = 1
addGestureRecognizer(tapGesture)
}
@objc public init() {
@ -113,6 +125,7 @@ public typealias ActionBlock = () -> Void
return label
}
/// H32 -> Head
@objc public static func commonLabelH32(_ scale: Bool) -> Label {
let label = Label.label()
label.styleH32(scale)
@ -140,12 +153,14 @@ public typealias ActionBlock = () -> Void
return label
}
/// B20 -> Body
@objc public static func commonLabelB20(_ scale: Bool) -> Label {
let label = Label.label()
label.styleB20(scale)
return label
}
/// Default
@objc open class func label() -> Label {
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 {
label.backgroundColor = UIColor.mfGet(forHex: backgroundColorHex)
}
@ -229,21 +248,27 @@ public typealias ActionBlock = () -> Void
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 "externalLink":
case "image":
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()
mutableString.append(NSAttributedString(string: " "))
mutableString.append(NSAttributedString(attachment: imageAttachment))
attributedString.insert(mutableString, at: location)
(label as? Label)?.hasAttachmentImage = true
case "font":
if let fontStyle = attribute.optionalStringForKey("style") {
@ -254,13 +279,13 @@ public typealias ActionBlock = () -> Void
} else {
let fontSize = attribute["size"] as? CGFloat
var font: UIFont?
if let fontName = attribute.optionalStringForKey("name") {
font = MFFonts.mfFont(withName: fontName, size: fontSize ?? label.font.pointSize)
} else if let fontSize = fontSize {
font = label.font.withSize(fontSize)
}
if let font = font {
attributedString.removeAttribute(.font, range: range)
attributedString.addAttribute(.font, value: font, range: range)
@ -272,9 +297,8 @@ public typealias ActionBlock = () -> Void
actionLabel.addActionAttributes(range: range, string: attributedString)
let accessibleAction = actionLabel.customAccessibilityAction(range: range)
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)
actionLabel.clauses.append(ActionableClause(range: range, actionBlock: actionBlock, hash: accessibleAction?.hash ?? -1))
default:
continue
}
@ -283,6 +307,10 @@ public typealias ActionBlock = () -> Void
}
}
//------------------------------------------------------
// MARK: - Methods
//------------------------------------------------------
@objc public func styleH1(_ scale: Bool) {
MFStyler.styleLabelH1(self, genericScaling: false)
setScale(scale)
@ -329,15 +357,28 @@ public typealias ActionBlock = () -> Void
if let originalAttributedString = originalAttributedString {
let attributedString = NSMutableAttributedString(attributedString: originalAttributedString)
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) {
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
} else if !MVMCoreGetterUtility.fequal(a: Float(standardFontSize), b: 0.0), let sizeObject: MFSizeObject = self.sizeObject ?? MFStyler.sizeObjectGeneric(forCurrentDevice: standardFontSize) {
self.font = self.font.withSize(sizeObject.getValueBased(onSize: size))
} else if !MVMCoreGetterUtility.fequal(a: Float(standardFontSize), b: 0.0), let sizeObject = sizeObject ?? MFStyler.sizeObjectGeneric(forCurrentDevice: standardFontSize) {
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.
Will provide one whitespace to the left of the icon
Appends an external link image to the end of the attributed string.
Will provide one whitespace to the left of the icon
*/
public func appendExternalLinkIcon() {
@objc public func appendExternalLinkIcon() {
guard let attributedText = attributedText else { return }
@ -372,43 +413,80 @@ public typealias ActionBlock = () -> Void
mutableString.append(NSAttributedString(string: " "))
mutableString.append(NSAttributedString(attachment: Label.getTextAttachmentImage(dimension: font.pointSize)))
self.attributedText = mutableString
hasAttachmentImage = true
}
/**
Insert external link icon anywhere within text of Label.
- Parameters:
- index: Location within the associated text to insert an external Link Icon
- Note: You will need to increment your current index by 2 for any changes to the text after inserting an icon.
Each icon insertion adds 2 additional characters to the overall text length.
- 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)
if index != 0 {
mutableString.insert(NSAttributedString(string: " "), at: index - 1)
}
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 imageAttachment = NSTextAttachment()
imageAttachment.image = MVMCoreUIUtility.imageNamed("externalLink")
imageAttachment.image = MVMCoreUIUtility.imageNamed(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 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
@ -419,7 +497,6 @@ extension Label {
attributedText = nil
textAlignment = .left
originalAttributedString = nil
hasAttachmentImage = false
styleB2(true)
accessibilityCustomActions = []
}
@ -493,9 +570,8 @@ extension Label {
Provides an actionable range of text.
- Attention: This method expects text to be set first. Otherwise, it will do nothing.
- Parameters:
- range: The range of text to be tapped.
- actionBlock: The code triggered when tapping the range of text.
- parameter range: The range of text to be tapped.
- parameter actionBlock: The code triggered when tapping the range of text.
*/
@objc public func addTappableLinkAttribute(range: NSRange, actionBlock: @escaping ActionBlock) {
@ -508,11 +584,10 @@ extension Label {
Provides an actionable range of text.
- Attention: This method expects text to be set first. Otherwise, it will do nothing.
- Parameters:
- range: The range of text to be tapped.
- actionMap:
- delegate:
- additionalData:
- parameter range: The range of text to be tapped.
- parameter actionMap:
- parameter delegate:
- parameter additionalData:
*/
@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) {
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) {
clause.performAction()
return
@ -538,29 +615,23 @@ 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 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.
for attr in attributedText.attributes(at: targetRange.location, effectiveRange: nil) {
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 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)
let textStorage = NSTextStorage(attributedString: mutableAttribString)
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)