diff --git a/MVMCoreUI.xcodeproj/project.pbxproj b/MVMCoreUI.xcodeproj/project.pbxproj index 374adbc5..d45f3185 100644 --- a/MVMCoreUI.xcodeproj/project.pbxproj +++ b/MVMCoreUI.xcodeproj/project.pbxproj @@ -167,6 +167,7 @@ D2E1FADD2268B25E00AEFD8C /* MoleculeTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2E1FADC2268B25E00AEFD8C /* MoleculeTableViewCell.swift */; }; D2E1FADF2268B8E700AEFD8C /* ThreeLayerTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2E1FADE2268B8E700AEFD8C /* ThreeLayerTableViewController.swift */; }; D2E1FAE12268E81D00AEFD8C /* MoleculeListTemplate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2E1FAE02268E81D00AEFD8C /* MoleculeListTemplate.swift */; }; + DB06250B2293456500B72DD3 /* LeftRightLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB06250A2293456500B72DD3 /* LeftRightLabelView.swift */; }; DBC4391822442197001AB423 /* CaretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC4391622442196001AB423 /* CaretView.swift */; }; DBC4391922442197001AB423 /* DashLine.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC4391722442197001AB423 /* DashLine.swift */; }; DBC4391B224421A0001AB423 /* CaretButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC4391A224421A0001AB423 /* CaretButton.swift */; }; @@ -338,6 +339,7 @@ D2E1FADC2268B25E00AEFD8C /* MoleculeTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoleculeTableViewCell.swift; sourceTree = ""; }; D2E1FADE2268B8E700AEFD8C /* ThreeLayerTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreeLayerTableViewController.swift; sourceTree = ""; }; D2E1FAE02268E81D00AEFD8C /* MoleculeListTemplate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoleculeListTemplate.swift; sourceTree = ""; }; + DB06250A2293456500B72DD3 /* LeftRightLabelView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LeftRightLabelView.swift; sourceTree = ""; }; DB891E822253FA8500022516 /* Label.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Label.swift; sourceTree = ""; }; DBC4391622442196001AB423 /* CaretView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CaretView.swift; sourceTree = ""; }; DBC4391722442197001AB423 /* DashLine.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashLine.swift; sourceTree = ""; }; @@ -590,6 +592,7 @@ children = ( DBC4391622442196001AB423 /* CaretView.swift */, DBC4391722442197001AB423 /* DashLine.swift */, + DB06250A2293456500B72DD3 /* LeftRightLabelView.swift */, D29DF17E21E69E2E003B2FB9 /* MFView.h */, D29DF17F21E69E2E003B2FB9 /* MFView.m */, D29DF31E21ED0CBA003B2FB9 /* LabelView.h */, @@ -950,6 +953,7 @@ D29DF29521E7ADB8003B2FB9 /* ProgrammaticScrollViewController.m in Sources */, D29DF16121E69996003B2FB9 /* MFViewController.m in Sources */, D2E1FAE12268E81D00AEFD8C /* MoleculeListTemplate.swift in Sources */, + DB06250B2293456500B72DD3 /* LeftRightLabelView.swift in Sources */, D22D1F47220496A30077CEC0 /* MVMCoreUISwitch.m in Sources */, D29DF28C21E7AC2B003B2FB9 /* ViewConstrainingView.m in Sources */, D29DF17B21E69E1F003B2FB9 /* PrimaryButton.m in Sources */, diff --git a/MVMCoreUI/Atoms/Views/Label.swift b/MVMCoreUI/Atoms/Views/Label.swift index a5ba4be0..a0733521 100644 --- a/MVMCoreUI/Atoms/Views/Label.swift +++ b/MVMCoreUI/Atoms/Views/Label.swift @@ -9,28 +9,49 @@ import MVMCore +public typealias ActionBlock = () -> Void -@objc open class Label: UILabel, MVMCoreViewProtocol, MVMCoreUIMoleculeViewProtocol, MVMCoreUIViewConstrainingProtocol { + +@objcMembers open class Label: UILabel, MVMCoreViewProtocol, MVMCoreUIMoleculeViewProtocol, MVMCoreUIViewConstrainingProtocol, MFButtonProtocol { //------------------------------------------------------ - // MARK: - Properties + // MARK: - General Properties //------------------------------------------------------ + + public var makeWholeViewClickable = false - // Set this property if you want updateView to update the font based on this standard and the size passed in. - @objc public var standardFontSize: CGFloat = 0.0 + /// 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 - // Set this to use a custom sizing object during updateView instead of the standard. - @objc public var sizeObject: MFSizeObject? - - @objc public var scaleSize: NSNumber? + /// Set this to use a custom sizing object during updateView instead of the standard. + public var sizeObject: MFSizeObject? + public var scaleSize: NSNumber? // Used for scaling the font in updateView. private var originalAttributedString: NSAttributedString? - @objc public var hasText: Bool { + public var hasText: Bool { guard let text = text, let attributedText = attributedText else { return false } return !text.isEmpty || !attributedText.string.isEmpty } + //------------------------------------------------------ + // MARK: - Multi-Action Text + //------------------------------------------------------ + + public var clauses: [ActionableClause] = [] { + didSet { isUserInteractionEnabled = !clauses.isEmpty } + } + + /// Used for tappable links in the text. + public struct ActionableClause { + var range: NSRange? + var actionBlock: ActionBlock? + + func performAction() { + actionBlock?() + } + } + //------------------------------------------------------ // MARK: - Initialization //------------------------------------------------------ @@ -41,6 +62,10 @@ import MVMCore numberOfLines = 0 lineBreakMode = .byWordWrapping translatesAutoresizingMaskIntoConstraints = false + + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(textLinkTapped(_:))) + tapGesture.numberOfTapsRequired = 1 + addGestureRecognizer(tapGesture) } @objc public init() { @@ -64,21 +89,24 @@ import MVMCore } //------------------------------------------------------ - // MARK: - Functions + // MARK: - Factory Functions //------------------------------------------------------ + /// H1 -> HeadlineLarge @objc public static func commonLabelH1(_ scale: Bool) -> Label { let label = Label.label() label.styleH1(scale) return label } + /// H2 -> Headline @objc public static func commonLabelH2(_ scale: Bool) -> Label { let label = Label.label() label.styleH2(scale) return label } + /// H3 -> SubHead @objc public static func commonLabelH3(_ scale: Bool) -> Label { let label = Label.label() label.styleH3(scale) @@ -91,18 +119,21 @@ import MVMCore return label } + /// B1 -> SubTitle @objc public static func commonLabelB1(_ scale: Bool) -> Label { let label = Label.label() label.styleB1(scale) return label } + /// B2 -> Body @objc public static func commonLabelB2(_ scale: Bool) -> Label { let label = Label.label() label.styleB2(scale) return label } + /// B3 -> Legal @objc public static func commonLabelB3(_ scale: Bool) -> Label { let label = Label.label() label.styleB3(scale) @@ -142,9 +173,9 @@ import MVMCore @objc public static func setUILabel(_ label: UILabel?, withJSON json: [AnyHashable: Any]?, delegate: DelegateObject?, additionalData: [AnyHashable: Any]?) { guard let label = label else { return } - + label.attributedText = nil label.text = json?.optionalStringForKey(KeyText) - + setLabel(label, withHTML: json?.optionalStringForKey("html")) if let backgroundColorHex = json?.optionalStringForKey(KeyBackgroundColor), !backgroundColorHex.isEmpty { @@ -173,21 +204,20 @@ import MVMCore let attributedString = NSMutableAttributedString(string: labelText, attributes: [NSAttributedString.Key.font: 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 else { continue } - + let range = NSRange(location: location, length: length) - + switch attributeType { case "underline": attributedString.addAttribute(.underlineStyle, value: NSUnderlineStyle.single.rawValue, range: range) - + case "strikethrough": attributedString.addAttribute(.strikethroughStyle, value: NSUnderlineStyle.thick.rawValue, range: range) - + case "color": if let colorHex = attribute.optionalStringForKey(KeyTextColor), !colorHex.isEmpty { attributedString.removeAttribute(.foregroundColor, range: range) @@ -214,6 +244,13 @@ import MVMCore attributedString.addAttribute(.font, value: font, range: range) } } + case "action": + guard let actionLabel = label as? Label else { continue } + + actionLabel.addTappableLinkAttribute(range: range, + actionMap: json, + additionalData: additionalData, + delegateObject: delegate) default: continue } @@ -222,6 +259,8 @@ import MVMCore } } + + //------------------------------------------------------ // MARK: - Methods //------------------------------------------------------ @@ -301,10 +340,10 @@ import MVMCore standardFontSize = 0 } } - - //------------------------------------------------------ - // MARK: - Atomization - //------------------------------------------------------ +} + +// MARK: - Atomization +extension Label { public func reset() { text = nil @@ -314,6 +353,7 @@ import MVMCore } @objc public func setWithJSON(_ json: [AnyHashable: Any]?, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) { + clauses = [] Label.setUILabel(self, withJSON: json, delegate: delegateObject, additionalData: additionalData) originalAttributedString = attributedText } @@ -323,7 +363,7 @@ import MVMCore } public func needsToBeConstrained() -> Bool { - return true; + return true } public func alignment() -> UIStackView.Alignment { @@ -331,6 +371,111 @@ import MVMCore } } +// MARK: - Multi-Action Functionality extension Label { - + + /// Reseting to default Label values. + @objc public func clearActionableClauses() { + + guard let attributedText = attributedText else { return } + let mutableAttributedString = NSMutableAttributedString(attributedString: attributedText) + + clauses.forEach { clause in + guard let range = clause.range else { return } + mutableAttributedString.removeAttribute(NSAttributedString.Key.underlineStyle, range: range) + } + + self.attributedText = mutableAttributedString + clauses = [] + } + + public func createActionBlockFrom(actionMap: [AnyHashable: Any]?, additionalData: [AnyHashable: Any]?, delegateObject: DelegateObject?) -> ActionBlock { + return { [weak self] in + if let wSelf = self, (delegateObject as? MVMCoreUIDelegateObject)?.buttonDelegate?.button?(wSelf, shouldPerformActionWithMap: actionMap, additionalData: additionalData) ?? true { + MVMCoreActionHandler.shared()?.handleAction(with: actionMap, additionalData: additionalData, delegateObject: delegateObject) + } + } + } + + fileprivate func setDefaultAttributes(range: NSRange) { + + guard let attributedText = attributedText else { return } + + let mutableAttributedString = NSMutableAttributedString(attributedString: attributedText) + mutableAttributedString.addAttributes([NSAttributedString.Key.underlineStyle: NSUnderlineStyle.single.rawValue], range: range) + + self.attributedText = mutableAttributedString + } + + /** + 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. + */ + @objc public func addTappableLinkAttribute(range: NSRange, actionBlock: @escaping ActionBlock) { + + setDefaultAttributes(range: range) + clauses.append(ActionableClause(range: range, actionBlock: actionBlock)) + } + + /** + 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: + */ + @objc public func addTappableLinkAttribute(range: NSRange, actionMap: [AnyHashable: Any]?, additionalData: [AnyHashable: Any]?, delegateObject: DelegateObject?) { + + setDefaultAttributes(range: range) + clauses.append(ActionableClause(range: range, + actionBlock: createActionBlockFrom(actionMap: actionMap, + additionalData: additionalData, + delegateObject: delegateObject))) + } + + @objc private func textLinkTapped(_ gesture: UITapGestureRecognizer) { + + for clause in clauses { + if let range = clause.range, gesture.didTapAttributedTextInLabel(self, inRange: range) { + clause.performAction() + return + } + } + } +} + +// MARK: - +extension UITapGestureRecognizer { + + func didTapAttributedTextInLabel(_ label: Label, inRange targetRange: NSRange) -> Bool { + + if label.makeWholeViewClickable { + return true + } + + guard let attributedText = label.attributedText else { return false } + + let layoutManager = NSLayoutManager() + let textContainer = NSTextContainer(size: .zero) + let textStorage = NSTextStorage(attributedString: attributedText) + + layoutManager.addTextContainer(textContainer) + textStorage.addLayoutManager(layoutManager) + + textContainer.lineFragmentPadding = 0.0 + textContainer.lineBreakMode = label.lineBreakMode + textContainer.maximumNumberOfLines = label.numberOfLines + textContainer.size = label.bounds.size + + let indexOfCharacter = layoutManager.characterIndex(for: location(in: label), in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil) + + return NSLocationInRange(indexOfCharacter, targetRange) + } } diff --git a/MVMCoreUI/Atoms/Views/LabelWithInternalButton.swift b/MVMCoreUI/Atoms/Views/LabelWithInternalButton.swift index 76693a6b..5cc6d50b 100644 --- a/MVMCoreUI/Atoms/Views/LabelWithInternalButton.swift +++ b/MVMCoreUI/Atoms/Views/LabelWithInternalButton.swift @@ -9,25 +9,22 @@ import MVMCore -public typealias ActionBlock = () -> Void -private typealias ActionableStringTuple = (front: String?, action: String?, end: String?) public typealias ActionObjectDelegate = NSObjectProtocol & MVMCoreActionDelegateProtocol public typealias ButtonObjectDelegate = NSObjectProtocol & ButtonDelegateProtocol public typealias CoreObjectActionLoadPresentDelegate = MVMCoreActionDelegateProtocol & MVMCoreLoadDelegateProtocol & MVMCorePresentationDelegateProtocol & NSObjectProtocol -@objcMembers open class LabelWithInternalButton: UIControl, MVMCoreViewProtocol, MFButtonProtocol { +@available(*, deprecated, message: "Use Label instead.") +@objcMembers open class LabelWithInternalButton: UIControl, MVMCoreViewProtocol, MFButtonProtocol, MVMCoreUIMoleculeViewProtocol { //------------------------------------------------------ // MARK: - Properties //------------------------------------------------------ - public var actionBlock: ActionBlock? public weak var label: Label? public var attributedText: NSAttributedString? { willSet(newAttributedText) { if let newAttribText = newAttributedText { - let mutableAttributedText = NSMutableAttributedString(attributedString: newAttribText) let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.lineSpacing = CGFloat(LabelWithInternalButtonLineSpace) @@ -62,16 +59,34 @@ public typealias CoreObjectActionLoadPresentDelegate = MVMCoreActionDelegateProt } } + public var actionBlock: ActionBlock? { + willSet(newActionBlock) { + if newActionBlock == nil { + label?.clearActionableClauses() + } else { + label?.clauses = [Label.ActionableClause(range: actionRange, actionBlock: newActionBlock)] + } + } + } + + private var frontRange: NSRange { + return NSRange(location: 0, length: frontText?.count ?? 0) + } + private var actionRange: NSRange { return NSRange(location: frontText?.count ?? 0, length: actionText?.count ?? 0) } - public var makeWholeViewClickable = false + private var backRange: NSRange { + return NSRange(location: (frontText?.count ?? 0) + (actionText?.count ?? 0), length: backText?.count ?? 0) + } + + public var makeWholeViewClickable = false { + willSet(newBool) { label?.makeWholeViewClickable = newBool } + } override open var isEnabled: Bool { - didSet { - alpha = isEnabled ? 1 : DisableOppacity - } + didSet { alpha = isEnabled ? 1 : DisableOppacity } } public var frontText: String? @@ -81,9 +96,7 @@ public typealias CoreObjectActionLoadPresentDelegate = MVMCoreActionDelegateProt private var internalText: String = "" public var text: String? { - get { - return internalText - } + get { return internalText } set { guard let text = newValue else { return } internalText = text @@ -101,117 +114,92 @@ public typealias CoreObjectActionLoadPresentDelegate = MVMCoreActionDelegateProt public init() { super.init(frame: .zero) - setup() + createLabel() + setLabelAttributes() } required public init?(coder: NSCoder) { super.init(coder: coder) - setup() + createLabel() + setLabelAttributes() } override public init(frame: CGRect) { super.init(frame: frame) - setup() + createLabel() + setLabelAttributes() } - // MARK: - legacy + // Legacy public init(frontText: String?, actionText: String?, backText: String?, actionBlock block: ActionBlock?) { super.init(frame: .zero) + createLabel() self.frontText = frontText self.actionText = actionText self.backText = backText - actionBlock = block text = getTextFromStringComponents() - setup() + actionBlock = block + setLabelAttributes() } public convenience init(actionMap: [AnyHashable: Any]?, additionalData: [AnyHashable: Any]?, delegateObject: DelegateObject?) { - self.init(frontText: actionMap?.optionalStringForKey(KeyTitlePrefix), - actionText: actionMap?.optionalStringForKey(KeyTitle), - backText: actionMap?.optionalStringForKey(KeyTitlePostfix), - actionMap: actionMap, additionalData: additionalData, - delegateObject: delegateObject) + self.init(frontText: actionMap?.optionalStringForKey(KeyTitlePrefix), actionText: actionMap?.optionalStringForKey(KeyTitle), backText: actionMap?.optionalStringForKey(KeyTitlePostfix), actionMap: actionMap, additionalData: additionalData, delegateObject: delegateObject) } public convenience init(frontText: String?, backText: String?, actionMap: [AnyHashable: Any]?, additionalData: [AnyHashable: Any]?, delegateObject: DelegateObject?) { - self.init(frontText: frontText, - actionText: actionMap?.optionalStringForKey(KeyTitle), - backText: backText, - actionMap: actionMap, - additionalData: additionalData, - delegateObject: delegateObject) + self.init(frontText: frontText, actionText: actionMap?.optionalStringForKey(KeyTitle), backText: backText, actionMap: actionMap, additionalData: additionalData, delegateObject: delegateObject) } public init(frontText: String?, actionText: String?, backText: String?, actionMap: [AnyHashable: Any]?, additionalData: [AnyHashable: Any]?, delegateObject: DelegateObject?) { - super.init(frame: CGRect.zero) + super.init(frame: .zero) setFrontText(frontText, actionText: actionText, actionMap: actionMap, backText: backText, additionalData: additionalData, delegateObject: delegateObject) } // Convenience Initializer which assumes that the clickable text will be embedded in curly braces {}. public convenience init(clickableTextEmbeddedInCurlyBraces fullText: String?, actionMapForClickableText actionMap: [AnyHashable: Any]?, additionalData: [AnyHashable: Any]?, delegateObject: DelegateObject?) { - self.init(text: fullText, - startTag: "{", - endTag: "}", - actionMap: actionMap, - additionalData: additionalData, - delegateObject: delegateObject) + self.init(text: fullText, startTag: "{", endTag: "}", actionMap: actionMap, additionalData: additionalData, delegateObject: delegateObject) } public init(text fullText: String?, startTag: String?, endTag: String?, actionMap: [AnyHashable: Any]?, additionalData: [AnyHashable: Any]?, delegateObject: DelegateObject?) { - super.init(frame: CGRect.zero) + super.init(frame: .zero) setText(fullText, startTag: startTag, endTag: endTag) - - actionBlock = { - MVMCoreActionHandler.shared()?.handleAction(with: actionMap, additionalData: additionalData, delegateObject: delegateObject) - } + actionBlock = label?.createActionBlockFrom(actionMap: actionMap, additionalData: additionalData, delegateObject: delegateObject) } //------------------------------------------------------ // MARK: - Configuration //------------------------------------------------------ - private func setup() { + /// Creates the Label that will be interacted with. + private func createLabel() { if self.label == nil { - let label = Label(frame: CGRect.zero) - - backgroundColor = .clear - label.isUserInteractionEnabled = false + let label = Label(frame: .zero) label.setContentCompressionResistancePriority(.required, for: .vertical) addSubview(label) - NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "H:|-0-[label]-0-|", options: .directionLeadingToTrailing, metrics: nil, views: ["label": label])) - NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "V:|-0-[label]-0-|", options: .directionLeadingToTrailing, metrics: nil, views: ["label": label])) + NSLayoutConstraint.constraintPinSubview(label, pinTop: true, pinBottom: true, pinLeft: true, pinRight: true) self.label = label label.sizeToFit() } + } + + /// Sets up label. Best to call sometime after setting the text. + private func setLabelAttributes() { // Adding the underline setAlternateActionTextAttributes([NSAttributedString.Key.underlineStyle: NSNumber(value: NSUnderlineStyle.single.rawValue)]) - self.label?.attributedText = attributedText - self.label?.accessibilityTraits = .button + label?.attributedText = attributedText + label?.accessibilityTraits = actionText?.isEmpty ?? true ? .staticText : .button } - + @objc public func setActionMap(_ actionMap: [AnyHashable: Any]?, additionalData: [AnyHashable: Any]?, delegateObject: DelegateObject?) { - weak var weakSelf: LabelWithInternalButton? = self - weak var weakButtonDelegate: ButtonDelegateProtocol? = (delegateObject as? MVMCoreUIDelegateObject)?.buttonDelegate - - actionBlock = { - var performAction = true - - if let wSelf = weakSelf, let wButtonDelegate = weakButtonDelegate, wButtonDelegate.responds(to: #selector(ButtonObjectDelegate.button(_:shouldPerformActionWithMap:additionalData:))) { - performAction = wButtonDelegate.button?(wSelf, shouldPerformActionWithMap: actionMap, additionalData: additionalData) ?? false - } - - if performAction { - MVMCoreActionHandler.shared()?.handleAction(with: actionMap, additionalData: additionalData, delegateObject: delegateObject) - } - } + actionBlock = label?.createActionBlockFrom(actionMap: actionMap, additionalData: additionalData, delegateObject: delegateObject) } //------------------------------------------------------ @@ -220,12 +208,13 @@ public typealias CoreObjectActionLoadPresentDelegate = MVMCoreActionDelegateProt @objc public func setFrontText(_ frontText: String?, actionText: String?, actionMap: [AnyHashable: Any]?, backText: String?, additionalData: [AnyHashable: Any]?, delegateObject: DelegateObject?) { + createLabel() self.frontText = frontText - setActionMap(actionMap, additionalData: additionalData, delegateObject: delegateObject) self.actionText = actionText self.backText = backText text = getTextFromStringComponents() - setup() + setActionMap(actionMap, additionalData: additionalData, delegateObject: delegateObject) + setLabelAttributes() } @objc public func setFrontText(_ frontText: String?, actionMap: [AnyHashable: Any]?, backText: String?, additionalData: [AnyHashable: Any]?, delegateObject: DelegateObject?) { @@ -244,10 +233,8 @@ public typealias CoreObjectActionLoadPresentDelegate = MVMCoreActionDelegateProt } if let b2Font = MFStyler.fontB2(), - let actions = actionMap, - actions.keys.count > 0, - let actionString = actions.optionalStringForKey(KeyTitle), - !actionString.isEmpty { + let actions = actionMap, actions.keys.count > 0, + let actionString = actions.optionalStringForKey(KeyTitle), !actionString.isEmpty { let actionStringOnLine = actionString + (addNewLine ? "\n" : " ") actionText = actionStringOnLine @@ -264,7 +251,6 @@ public typealias CoreObjectActionLoadPresentDelegate = MVMCoreActionDelegateProt } attributedText = mutableAttributedString - // Added this line for underlining setAlternateActionTextAttributes([NSAttributedString.Key.underlineStyle: NSNumber(value: NSUnderlineStyle.single.rawValue)]) } @@ -276,51 +262,6 @@ public typealias CoreObjectActionLoadPresentDelegate = MVMCoreActionDelegateProt label?.attributedText = attributedText } - //------------------------------------------------------ - // MARK: - UIControl Override - //------------------------------------------------------ - - override open func touchesEnded(_ touches: Set, with event: UIEvent?) { - - if areTouches(inActionString: touches) { - sendActions(for: .touchUpInside) - if let action = actionBlock { - action() - } - } else { - sendActions(for: .touchUpOutside) - } - } - - private func areTouches(inActionString touches: Set?) -> Bool { - - if UIAccessibility.isVoiceOverRunning || makeWholeViewClickable { - return true - } - - let location: CGPoint? = touches?.first?.location(in: label) - let actionString = actionText - let index: Int = actionRange.location - let rangeArray = getRangeArrayOfWords(in: actionString, withInitalIndex: index) - let rectArray = getRectArray(fromRangeArray: rangeArray) - var result = false - - for aValueOfRect in rectArray as? [NSValue] ?? [] { - let wordRect: CGRect = aValueOfRect.cgRectValue - - if let position = location, wordRect.contains(position) { - result = true - break - } else if wordRect.origin.x == 0 && wordRect.origin.y == 0 && wordRect.size.height == 0 && wordRect.size.width == 0 { - // Incase word rect is not found for any reason, make the whole label to be clicable to avoid non functioning link in production. - result = true - break - } - } - - return result - } - //------------------------------------------------------ // MARK: - Helper //------------------------------------------------------ @@ -338,40 +279,6 @@ public typealias CoreObjectActionLoadPresentDelegate = MVMCoreActionDelegateProt return "\(frontText ?? "")\(actionText ?? "")\(backText ?? "")" } - private func getRangeArrayOfWords(in string: String?, withInitalIndex index: Int) -> [Any]? { - - var index = index - let words = string?.components(separatedBy: " ") - var rangeArray = [AnyHashable]() - - for subString in words ?? [] { - let finalSubString = subString + " " - let wordIndex: Int = index - let length: Int = finalSubString.count - let subStringRange = NSRange(location: wordIndex, length: length) - let rangeValue = NSValue(range: subStringRange) - rangeArray.append(rangeValue) - index += length - } - - return rangeArray - } - - private func getRectArray(fromRangeArray rangeArray: [Any]?) -> [Any]? { - - var rectArray = [AnyHashable]() - - for aValueOfRange in rangeArray as? [NSValue] ?? [] { - let wordRange: NSRange = aValueOfRange.rangeValue - if let rect: CGRect = label?.boundingRect(forCharacterRange: wordRange) { - let rectValue = NSValue(cgRect: rect) - rectArray.append(rectValue) - } - } - - return rectArray - } - @objc public func setCurlyBracedText(_ text: String) { setText(text, startTag: "{", endTag: "}") @@ -379,47 +286,24 @@ public typealias CoreObjectActionLoadPresentDelegate = MVMCoreActionDelegateProt private func setText(_ text: String?, startTag: String?, endTag: String?) { - let actionableTuple: ActionableStringTuple = rangeOfText(text, startTag: startTag, endTag: endTag) + createLabel() + frontText = text - if let text = text, - let leftTag = startTag, - text.contains(leftTag), - let rightTag = endTag, - text.contains(rightTag), - let front = actionableTuple.front, - let middle = actionableTuple.action, - let end = actionableTuple.end { - - frontText = front.trimmingCharacters(in: .whitespaces) - actionText = middle.trimmingCharacters(in: .whitespaces) - backText = end.trimmingCharacters(in: .whitespaces) - } else { - frontText = text - } - - self.text = getTextFromStringComponents() - setup() - } - - private func rangeOfText(_ text: String?, startTag: String?, endTag: String?) -> ActionableStringTuple { - - var actionableTuple: ActionableStringTuple = (front: nil, action: nil, end: nil) - - guard let text = text else { return actionableTuple } - - if let leftTag = startTag, text.contains(leftTag) { - - let firstHalf = text.components(separatedBy: leftTag) - actionableTuple.front = firstHalf.first - - if let rightTag = endTag, text.contains(rightTag) { - let secondHalf = firstHalf[1].components(separatedBy: rightTag) - actionableTuple.action = secondHalf[0] - actionableTuple.end = secondHalf[1] + if let text = text { + var initialSegments = [String]() + if let leftTag = startTag, text.contains(leftTag) { + initialSegments = text.components(separatedBy: leftTag) + frontText = initialSegments[0].trimmingCharacters(in: .whitespaces) + + if let rightTag = endTag, text.contains(rightTag) { + let secondPart = initialSegments[1].components(separatedBy: rightTag) + actionText = secondPart[0].trimmingCharacters(in: .whitespaces) + backText = secondPart[1].trimmingCharacters(in: .whitespaces) + } } } - - return actionableTuple + self.text = getTextFromStringComponents() + setLabelAttributes() } // Reset the text and action map @@ -459,49 +343,34 @@ public typealias CoreObjectActionLoadPresentDelegate = MVMCoreActionDelegateProt public func setAlternateNormalTextAttributes(_ attributes: [AnyHashable: Any]?) { - guard let _attributedText = attributedText, let _attributes = attributes as? [NSAttributedString.Key: Any] else { return } - let attributedString = NSMutableAttributedString(attributedString: _attributedText) + guard let attributedText = attributedText, let attributes = attributes as? [NSAttributedString.Key: Any] else { return } + let attributedString = NSMutableAttributedString(attributedString: attributedText) if !attributedString.string.isEmpty { - attributedString.addAttributes(_attributes, range: getFrontRange()) - attributedString.addAttributes(_attributes, range: getBackRange()) + attributedString.addAttributes(attributes, range: frontRange) + attributedString.addAttributes(attributes, range: backRange) } - attributedText = attributedString - } - - private func getFrontRange() -> NSRange { - - return NSRange(location: 0, length: frontText?.count ?? 0) - } - - private func getBackRange() -> NSRange { - - let textLocation: Int = (frontText?.count ?? 0) + (actionText?.count ?? 0) - let rangeLength: Int = backText?.count ?? 0 - - return NSRange(location: textLocation, length: rangeLength) + self.attributedText = attributedString } @objc public func setActionTextString(_ actionText: String?) { + createLabel() self.actionText = actionText text = getTextFromStringComponents() - setup() + setLabelAttributes() } /// Used to just reset the texts and actions if already initialized @objc public func reset(withActionMap actionMap: [AnyHashable: Any]?, additionalData: [AnyHashable: Any]?, delegateObject: DelegateObject?) { + createLabel() frontText = actionMap?.optionalStringForKey(KeyTitlePrefix) actionText = actionMap?.optionalStringForKey(KeyTitle) backText = actionMap?.optionalStringForKey(KeyTitlePostfix) - - actionBlock = { - MVMCoreActionHandler.shared()?.handleAction(with: actionMap, additionalData: additionalData, delegateObject: delegateObject) - } - text = getTextFromStringComponents() - setup() + actionBlock = label?.createActionBlockFrom(actionMap: actionMap, additionalData: additionalData, delegateObject: delegateObject) + setLabelAttributes() } //------------------------------------------------------ @@ -539,20 +408,18 @@ public typealias CoreObjectActionLoadPresentDelegate = MVMCoreActionDelegateProt @available(*, deprecated) public init(frontText: String?, actionText: String?, backText: String?, actionMap: [AnyHashable: Any]?, additionalData: [AnyHashable: Any]?, actionDelegate delegate: ActionObjectDelegate?, buttonDelegate: ButtonObjectDelegate?) { - super.init(frame: CGRect.zero) + super.init(frame: .zero) setFrontText(frontText, actionText: actionText, actionMap: actionMap, backText: backText, additionalData: additionalData, delegate: delegate, buttonDelegate: buttonDelegate) } @available(*, deprecated) public convenience init(actionMap: [AnyHashable: Any]?, additionalData: [AnyHashable: Any]?, actionDelegate delegate: ActionObjectDelegate?, buttonDelegate: ButtonObjectDelegate?) { - self.init(frontText: actionMap?.optionalStringForKey(KeyTitlePrefix), actionText: actionMap?.optionalStringForKey(KeyTitle), backText: actionMap?.optionalStringForKey(KeyTitlePostfix), actionMap: actionMap, additionalData: additionalData, actionDelegate: delegate, buttonDelegate: buttonDelegate) } @available(*, deprecated) public convenience init(frontText: String?, backText: String?, actionMap: [AnyHashable: Any]?, additionalData: [AnyHashable: Any]?, actionDelegate delegate: ActionObjectDelegate?, buttonDelegate: ButtonObjectDelegate?) { - self.init(frontText: frontText, actionText: actionMap?.optionalStringForKey(KeyTitle), backText: backText, actionMap: actionMap, additionalData: additionalData, actionDelegate: delegate, buttonDelegate: buttonDelegate) } @@ -583,29 +450,18 @@ public typealias CoreObjectActionLoadPresentDelegate = MVMCoreActionDelegateProt setText(fullText, startTag: startTag, endTag: endTag) - weak var weakDelegate: ActionObjectDelegate? = delegate - - actionBlock = { - MVMCoreActionHandler.shared()?.handleAction(with: actionMap, additionalData: additionalData, delegate: weakDelegate as? CoreObjectActionLoadPresentDelegate) + actionBlock = { [weak delegate] in + MVMCoreActionHandler.shared()?.handleAction(with: actionMap, additionalData: additionalData, delegate: delegate as? CoreObjectActionLoadPresentDelegate) } } @available(*, deprecated) private func setActionMap(_ actionMap: [AnyHashable: Any]?, additionalData: [AnyHashable: Any]?, actionDelegate delegate: ActionObjectDelegate?, buttonDelegate: ButtonObjectDelegate?) { - weak var weakSelf: LabelWithInternalButton? = self - weak var weakDelegate: ActionObjectDelegate? = delegate - weak var weakButtonDelegate: ButtonObjectDelegate? = buttonDelegate + actionBlock = { [weak self, weak delegate, weak buttonDelegate] in - actionBlock = { - var performAction = true - - if let wSelf = weakSelf, let wButtonDelegate = weakButtonDelegate, wButtonDelegate.responds(to: #selector(ButtonObjectDelegate.button(_:shouldPerformActionWithMap:additionalData:))) { - performAction = wButtonDelegate.button?(wSelf, shouldPerformActionWithMap: actionMap, additionalData: additionalData) ?? false - } - - if performAction { - MVMCoreActionHandler.shared()?.handleAction(with: actionMap, additionalData: additionalData, delegate: weakDelegate as? CoreObjectActionLoadPresentDelegate) + if let wSelf = self, buttonDelegate?.button?(wSelf, shouldPerformActionWithMap: actionMap, additionalData: additionalData) ?? true { + MVMCoreActionHandler.shared()?.handleAction(with: actionMap, additionalData: additionalData, delegate: delegate as? CoreObjectActionLoadPresentDelegate) } } } @@ -618,12 +474,13 @@ public typealias CoreObjectActionLoadPresentDelegate = MVMCoreActionDelegateProt @available(*, deprecated) @objc public func setFrontText(_ frontText: String?, actionText: String?, actionMap: [AnyHashable: Any]?, backText: String?, additionalData: [AnyHashable: Any]?, delegate: ActionObjectDelegate?, buttonDelegate: ButtonObjectDelegate?) { + createLabel() self.frontText = frontText - setActionMap(actionMap, additionalData: additionalData, actionDelegate: delegate, buttonDelegate: buttonDelegate) self.actionText = actionText self.backText = backText text = getTextFromStringComponents() - setup() + setActionMap(actionMap, additionalData: additionalData, actionDelegate: delegate, buttonDelegate: buttonDelegate) + setLabelAttributes() } @available(*, deprecated) @@ -644,10 +501,8 @@ public typealias CoreObjectActionLoadPresentDelegate = MVMCoreActionDelegateProt } if let b2Font = MFStyler.fontB2(), - let actions = actionMap, - actions.keys.count > 0, - let actionString = actions.optionalStringForKey(KeyTitle), - !actionString.isEmpty { + let actions = actionMap, actions.keys.count > 0, + let actionString = actions.optionalStringForKey(KeyTitle), !actionString.isEmpty { let actionStringOnLine = actionString + (addNewLine ? "\n" : " ") actionText = actionStringOnLine @@ -664,7 +519,6 @@ public typealias CoreObjectActionLoadPresentDelegate = MVMCoreActionDelegateProt } attributedText = mutableAttributedString - // Added this line for underlining setAlternateActionTextAttributes([NSAttributedString.Key.underlineStyle: NSNumber(value: NSUnderlineStyle.single.rawValue)]) } @@ -712,23 +566,22 @@ public typealias CoreObjectActionLoadPresentDelegate = MVMCoreActionDelegateProt @available(*, deprecated) @objc public func reset(withActionMap actionMap: [AnyHashable: Any]?, additionalData: [AnyHashable: Any]?, delegate: ActionObjectDelegate?) { + createLabel() frontText = actionMap?.optionalStringForKey(KeyTitlePrefix) actionText = actionMap?.optionalStringForKey(KeyTitle) backText = actionMap?.optionalStringForKey(KeyTitlePostfix) - - weak var weakDelegate: ActionObjectDelegate? = delegate - - actionBlock = { - MVMCoreActionHandler.shared()?.handleAction(with: actionMap, additionalData: additionalData, delegate: weakDelegate as? CoreObjectActionLoadPresentDelegate) - } - text = getTextFromStringComponents() - setup() + + actionBlock = { [weak delegate] in + MVMCoreActionHandler.shared()?.handleAction(with: actionMap, additionalData: additionalData, delegate: delegate as? CoreObjectActionLoadPresentDelegate) + } + setLabelAttributes() } -} -// MARK: - Atomization -extension LabelWithInternalButton: MVMCoreUIMoleculeViewProtocol { + //------------------------------------------------------ + // MARK: - Atomization + //------------------------------------------------------ + // Default values for view. @objc open func setAsMolecule() { diff --git a/MVMCoreUI/Atoms/Views/LeftRightLabelView.swift b/MVMCoreUI/Atoms/Views/LeftRightLabelView.swift new file mode 100644 index 00000000..1ca1402a --- /dev/null +++ b/MVMCoreUI/Atoms/Views/LeftRightLabelView.swift @@ -0,0 +1,183 @@ +// +// LeftRightLabelView.swift +// MVMCoreUI +// +// Created by Christiano, Kevin on 5/20/19. +// Copyright © 2019 Verizon Wireless. All rights reserved. +// + +import Foundation + + +@objcMembers open class LeftRightLabelView: ViewConstrainingView { + //------------------------------------------------------ + // MARK: - Outlets + //------------------------------------------------------ + + let leftTextLabel = Label.commonLabelB1(true) + let rightTextLabel = Label.commonLabelB1(true) + + //------------------------------------------------------ + // MARK: - Constraints + //------------------------------------------------------ + + var rightTextLabelLeading: NSLayoutConstraint? + var leftTextLabelTrailing: NSLayoutConstraint? + + //------------------------------------------------------ + // MARK: - Initialization + //------------------------------------------------------ + + public init() { + super.init(frame: .zero) + } + + public override init(frame: CGRect) { + super.init(frame: frame) + } + + required public init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + } + + public convenience init(json: [AnyHashable: Any]?, delegateObject: DelegateObject?, additionalData: [AnyHashable: Any]?) { + self.init() + setWithJSON(json, delegateObject: delegateObject, additionalData: additionalData) + } + + override open func setupView() { + super.setupView() + + guard subviews.isEmpty else { return } + + addSubview(leftTextLabel) + addSubview(rightTextLabel) + + leftTextLabel.textAlignment = .left + rightTextLabel.textAlignment = .right + + constrainBothLabels() + } + + //------------------------------------------------------ + // MARK: - View Lifecycle + //------------------------------------------------------ + + override open func updateView(_ size: CGFloat) { + super.updateView(size) + + leftTextLabel.updateView(size) + rightTextLabel.updateView(size) + + // Resolves text layout issues found between both dynamically sized labels, number is not exact but performs as required. + if leftTextLabel.hasText && rightTextLabel.hasText { + let padding = MFStyler.defaultHorizontalPadding(forSize: size) * 2 + let maximumTextWidth = (size - (padding + 16)) * 0.4 + // Subtracting 10 resolves issues of SE and iPad + rightTextLabel.preferredMaxLayoutWidth = round(maximumTextWidth) - 10 + } else { + rightTextLabel.preferredMaxLayoutWidth = 0 + } + } + + //------------------------------------------------------ + // MARK: - Setup + //------------------------------------------------------ + + private func constrainBothLabels() { + + leftTextLabel.topAnchor.constraint(equalTo: topAnchor).isActive = true + leftTextLabel.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor).isActive = true + + let leftTextBottom = leftTextLabel.bottomAnchor.constraint(equalTo: bottomAnchor) + leftTextBottom.priority = UILayoutPriority(249) + leftTextBottom.isActive = true + + bottomAnchor.constraint(greaterThanOrEqualTo: leftTextLabel.bottomAnchor).isActive = true + + rightTextLabelLeading = rightTextLabel.leadingAnchor.constraint(equalTo: leftTextLabel.trailingAnchor, constant: 16) + rightTextLabelLeading?.isActive = true + + rightTextLabel.topAnchor.constraint(equalTo: topAnchor).isActive = true + let rightLayout = layoutMarginsGuide.trailingAnchor.constraint(equalTo: rightTextLabel.trailingAnchor) + rightLayout.priority = UILayoutPriority(rawValue: 995) + rightLayout.isActive = true + + let rightTextBottom = rightTextLabel.bottomAnchor.constraint(equalTo: bottomAnchor) + rightTextBottom.priority = UILayoutPriority(rawValue: 249) + rightTextBottom.isActive = true + + bottomAnchor.constraint(greaterThanOrEqualTo: rightTextLabel.bottomAnchor).isActive = true + + let leftTextWidth = leftTextLabel.widthAnchor.constraint(greaterThanOrEqualTo: layoutMarginsGuide.widthAnchor, multiplier: 0.6) + leftTextWidth.priority = UILayoutPriority(rawValue: 995) + leftTextWidth.isActive = true + + let rightTextWidth = rightTextLabel.widthAnchor.constraint(lessThanOrEqualTo: layoutMarginsGuide.widthAnchor, multiplier: 0.4) + rightTextWidth.priority = UILayoutPriority(rawValue: 906) + rightTextWidth.isActive = true + + leftTextLabel.setContentHuggingPriority(UILayoutPriority(rawValue: 901), for: .horizontal) + rightTextLabel.setContentHuggingPriority(UILayoutPriority(rawValue: 902), for: .horizontal) + + leftTextLabel.setContentHuggingPriority(.required, for: .vertical) + rightTextLabel.setContentHuggingPriority(.required, for: .vertical) + + leftTextLabel.setContentCompressionResistancePriority(.required, for: .vertical) + rightTextLabel.setContentCompressionResistancePriority(.required, for: .vertical) + rightTextLabel.setContentCompressionResistancePriority(UILayoutPriority(rawValue: 902), for: .horizontal) + } + + private func constrainLeftLabel() { + + deactivateMiddleConstraint() + leftTextLabelTrailing = layoutMarginsGuide.trailingAnchor.constraint(equalTo: leftTextLabel.trailingAnchor) + leftTextLabelTrailing?.isActive = true + } + + private func constrainRightLabel() { + + deactivateMiddleConstraint() + rightTextLabelLeading = rightTextLabel.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor) + rightTextLabelLeading?.isActive = true + } + + override open func reset() { + super.reset() + + deactivateMiddleConstraint() + constrainBothLabels() + leftTextLabel.text = "" + rightTextLabel.text = "" + backgroundColor = nil + } + + private func deactivateMiddleConstraint() { + + leftTextLabelTrailing?.isActive = false + rightTextLabelLeading?.isActive = false + } + + //------------------------------------------------------ + // MARK: - Atomization + //------------------------------------------------------ + + open override func setWithJSON(_ json: [AnyHashable: Any]?, delegateObject: DelegateObject?, additionalData: [AnyHashable: Any]?) { + super.setWithJSON(json, delegateObject: delegateObject as? MVMCoreUIDelegateObject, additionalData: additionalData) + + guard let dictionary = json else { return } + + leftTextLabel.setWithJSON(dictionary.optionalDictionaryForKey("leftText"), delegateObject: delegateObject as? MVMCoreUIDelegateObject, additionalData: additionalData) + rightTextLabel.setWithJSON(dictionary.optionalDictionaryForKey("rightText"), delegateObject: delegateObject as? MVMCoreUIDelegateObject, additionalData: additionalData) + + if let backgroundColorHex = dictionary[KeyBackgroundColor] as? String { + backgroundColor = UIColor.mfGet(forHex: backgroundColorHex) + } + + if !leftTextLabel.hasText { + constrainRightLabel() + } else if !rightTextLabel.hasText { + constrainLeftLabel() + } + } +} diff --git a/MVMCoreUI/Atoms/Views/ViewConstrainingView.m b/MVMCoreUI/Atoms/Views/ViewConstrainingView.m index a7412b93..02d965b7 100644 --- a/MVMCoreUI/Atoms/Views/ViewConstrainingView.m +++ b/MVMCoreUI/Atoms/Views/ViewConstrainingView.m @@ -274,6 +274,11 @@ [super setupView]; self.translatesAutoresizingMaskIntoConstraints = NO; self.backgroundColor = [UIColor clearColor]; + if (@available(iOS 11.0, *)) { + self.directionalLayoutMargins = NSDirectionalEdgeInsetsZero; + } else { + self.layoutMargins = UIEdgeInsetsZero; + } } - (void)updateView:(CGFloat)size { diff --git a/MVMCoreUI/Molecules/TwoButtonView.swift b/MVMCoreUI/Molecules/TwoButtonView.swift index c5085cce..f2a8bc44 100644 --- a/MVMCoreUI/Molecules/TwoButtonView.swift +++ b/MVMCoreUI/Molecules/TwoButtonView.swift @@ -44,8 +44,8 @@ import UIKit if let backgroundColorString = json?.optionalStringForKey(KeyBackgroundColor) { backgroundColor = UIColor.mfGet(forHex: backgroundColorString) } - let primaryButtonMap = json?.optionalDictionaryForKey("primaryButton") - let secondaryButtonMap = json?.optionalDictionaryForKey("secondaryButton") + let primaryButtonMap = json?.optionalDictionaryForKey(KeyPrimaryButton) + let secondaryButtonMap = json?.optionalDictionaryForKey(KeySecondaryButton) set(primaryButtonJSON: primaryButtonMap, secondaryButtonJSON: secondaryButtonMap, delegateObject: delegateObject, additionalData: additionalData) } diff --git a/MVMCoreUI/OtherHandlers/MVMCoreUIMoleculeMappingObject.m b/MVMCoreUI/OtherHandlers/MVMCoreUIMoleculeMappingObject.m index 0518299f..ebac9da4 100644 --- a/MVMCoreUI/OtherHandlers/MVMCoreUIMoleculeMappingObject.m +++ b/MVMCoreUI/OtherHandlers/MVMCoreUIMoleculeMappingObject.m @@ -42,7 +42,8 @@ @"checkbox": MVMCoreUICheckBox.class, @"listItem": MoleculeTableViewCell.class, @"switchLineItem": SwitchLineItem.class, - @"switch": Switch.class + @"switch": Switch.class, + @"leftRightLabelView": LeftRightLabelView.class } mutableCopy]; }); return mapping;