diff --git a/MVMCoreUI.xcodeproj/project.pbxproj b/MVMCoreUI.xcodeproj/project.pbxproj index 08f4af0f..ab895b83 100644 --- a/MVMCoreUI.xcodeproj/project.pbxproj +++ b/MVMCoreUI.xcodeproj/project.pbxproj @@ -21,7 +21,7 @@ 01DF567021FA5AB300CC099B /* TextFieldListFormViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01DF566F21FA5AB300CC099B /* TextFieldListFormViewController.swift */; }; 01E569D3223FFFA500327251 /* ThreeLayerViewController.swift in Headers */ = {isa = PBXBuildFile; fileRef = D2A5146A2214905000345BFB /* ThreeLayerViewController.swift */; settings = {ATTRIBUTES = (Public, ); }; }; B8200E152280C4CF007245F4 /* ProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8200E142280C4CF007245F4 /* ProgressBar.swift */; }; - B8200E192281DC1A007245F4 /* ProgressBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8200E182281DC1A007245F4 /* ProgressBarView.swift */; }; + B8200E192281DC1A007245F4 /* ProgressBarWithLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8200E182281DC1A007245F4 /* ProgressBarWithLabel.swift */; }; D206997721FB8A0B00CAE0DE /* MVMCoreUINavigationController.h in Headers */ = {isa = PBXBuildFile; fileRef = D206997521FB8A0B00CAE0DE /* MVMCoreUINavigationController.h */; settings = {ATTRIBUTES = (Public, ); }; }; D206997821FB8A0B00CAE0DE /* MVMCoreUINavigationController.m in Sources */ = {isa = PBXBuildFile; fileRef = D206997621FB8A0B00CAE0DE /* MVMCoreUINavigationController.m */; }; D20A9A5E2243D3E300ADE781 /* TwoButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20A9A5D2243D3E300ADE781 /* TwoButtonView.swift */; }; @@ -169,6 +169,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 */; }; @@ -190,7 +191,7 @@ 01DF55DF21F8FAA800CC099B /* MFTextFieldListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MFTextFieldListView.swift; sourceTree = ""; }; 01DF566F21FA5AB300CC099B /* TextFieldListFormViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFieldListFormViewController.swift; sourceTree = ""; }; B8200E142280C4CF007245F4 /* ProgressBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressBar.swift; sourceTree = ""; }; - B8200E182281DC1A007245F4 /* ProgressBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressBarView.swift; sourceTree = ""; }; + B8200E182281DC1A007245F4 /* ProgressBarWithLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressBarWithLabel.swift; sourceTree = ""; }; D206997521FB8A0B00CAE0DE /* MVMCoreUINavigationController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MVMCoreUINavigationController.h; sourceTree = ""; }; D206997621FB8A0B00CAE0DE /* MVMCoreUINavigationController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MVMCoreUINavigationController.m; sourceTree = ""; }; D20A9A5D2243D3E300ADE781 /* TwoButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwoButtonView.swift; sourceTree = ""; }; @@ -342,6 +343,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 = ""; }; @@ -473,6 +475,7 @@ 01004F2F22721C3800991ECC /* RadioButton.swift */, D2E1FADC2268B25E00AEFD8C /* MoleculeTableViewCell.swift */, B8200E142280C4CF007245F4 /* ProgressBar.swift */, + B8200E182281DC1A007245F4 /* ProgressBarWithLabel.swift */, B8200E182281DC1A007245F4 /* ProgressBarView.swift */, 0116A4E4228B19640094F3ED /* RadioButtonModel.swift */, ); @@ -596,6 +599,7 @@ children = ( DBC4391622442196001AB423 /* CaretView.swift */, DBC4391722442197001AB423 /* DashLine.swift */, + DB06250A2293456500B72DD3 /* LeftRightLabelView.swift */, D29DF17E21E69E2E003B2FB9 /* MFView.h */, D29DF17F21E69E2E003B2FB9 /* MFView.m */, D29DF31E21ED0CBA003B2FB9 /* LabelView.h */, @@ -958,6 +962,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 */, @@ -983,7 +988,7 @@ D2A514632213643100345BFB /* MoleculeStackCenteredTemplate.swift in Sources */, D29DF32421ED0DA2003B2FB9 /* TextButtonView.m in Sources */, D29DF29E21E7AE3B003B2FB9 /* MFStyler.m in Sources */, - B8200E192281DC1A007245F4 /* ProgressBarView.swift in Sources */, + B8200E192281DC1A007245F4 /* ProgressBarWithLabel.swift in Sources */, D2A514592211C53C00345BFB /* MVMCoreUIMoleculeMappingObject.m in Sources */, 0105618D224BBE7700E1557D /* FormValidator.swift in Sources */, D22D1F1B220341F60077CEC0 /* MVMCoreUICheckBox.m in Sources */, diff --git a/MVMCoreUI/Atoms/TextFields/MFDigitTextField.h b/MVMCoreUI/Atoms/TextFields/MFDigitTextField.h index 936ed017..4be970da 100644 --- a/MVMCoreUI/Atoms/TextFields/MFDigitTextField.h +++ b/MVMCoreUI/Atoms/TextFields/MFDigitTextField.h @@ -8,6 +8,7 @@ #import +@class MFDigitTextBox; @interface MFDigitTextField : MFTextField + (nullable instancetype)mfDigitTextFieldWithNumberOfDigits:(NSUInteger)numberOfDigits; @@ -18,4 +19,6 @@ - (void)setAsSecureTextEntry:(BOOL)secureTextEntry; +@property (nullable, nonatomic, strong) NSArray *textFields; +//Be careful when using this directly. There's a possiblity that you may break the view. @end diff --git a/MVMCoreUI/Atoms/TextFields/MFDigitTextField.m b/MVMCoreUI/Atoms/TextFields/MFDigitTextField.m index d04cd56c..084ba338 100644 --- a/MVMCoreUI/Atoms/TextFields/MFDigitTextField.m +++ b/MVMCoreUI/Atoms/TextFields/MFDigitTextField.m @@ -29,7 +29,7 @@ @property (nullable, nonatomic, weak) IBOutlet NSLayoutConstraint *messageToTextFieldPin; @property (nullable, nonatomic, weak) IBOutlet NSLayoutConstraint *labelToTextFieldPin; -@property (nullable, nonatomic, strong) NSArray *textFields; + @property (nonatomic) BOOL switchedAutomatically; @end diff --git a/MVMCoreUI/Atoms/TextFields/MFTextField.m b/MVMCoreUI/Atoms/TextFields/MFTextField.m index 4bdbc8d3..95a85ff5 100644 --- a/MVMCoreUI/Atoms/TextFields/MFTextField.m +++ b/MVMCoreUI/Atoms/TextFields/MFTextField.m @@ -576,7 +576,7 @@ - (BOOL)isValidField { - return self.valid; + return self.isValid; } - (nullable NSString *)formFieldName { diff --git a/MVMCoreUI/Atoms/Views/Label.swift b/MVMCoreUI/Atoms/Views/Label.swift index f2792081..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,14 +173,10 @@ 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 textColorHex = json?.optionalStringForKey(KeyTextColor), !textColorHex.isEmpty { - label.textColor = UIColor.mfGet(forHex: textColorHex) - } + setLabel(label, withHTML: json?.optionalStringForKey("html")) if let backgroundColorHex = json?.optionalStringForKey(KeyBackgroundColor), !backgroundColorHex.isEmpty { label.backgroundColor = UIColor.mfGet(forHex: backgroundColorHex) @@ -157,52 +184,73 @@ import MVMCore label.accessibilityLabel = json?.optionalStringForKey("accessibilityText") - let fontSize = json?["fontSize"] as? CGFloat + if let fontStyle = json?.optionalStringForKey("fontStyle") { + MFStyler.styleLabel(label, withStyle: fontStyle) + } else { + let fontSize = json?["fontSize"] as? CGFloat + + if let fontName = json?.optionalStringForKey("fontName") { + label.font = MFFonts.mfFont(withName: fontName, size: fontSize ?? label.font.pointSize) + } else if let fontSize = fontSize { + label.font = label.font.withSize(fontSize) + } + } - if let fontName = json?.optionalStringForKey("fontName") { - label.font = MFFonts.mfFont(withName: fontName, size: fontSize ?? label.font.pointSize) - } else if let fontSize = fontSize { - label.font = label.font.withSize(fontSize) + if let textColorHex = json?.optionalStringForKey(KeyTextColor), !textColorHex.isEmpty { + label.textColor = UIColor.mfGet(forHex: textColorHex) } if let attributes = json?.arrayForKey("attributes"), let labelText = label.text { 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) attributedString.addAttribute(.foregroundColor, value: UIColor.mfGet(forHex: colorHex), range: range) } case "font": - 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 { + if let fontStyle = attribute.optionalStringForKey("style") { + let styles = MFStyler.styleGetAttributedString("0", withStyle: fontStyle) attributedString.removeAttribute(.font, range: range) - attributedString.addAttribute(.font, value: 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 ?? 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) + } } + case "action": + guard let actionLabel = label as? Label else { continue } + + actionLabel.addTappableLinkAttribute(range: range, + actionMap: json, + additionalData: additionalData, + delegateObject: delegate) default: continue } @@ -211,6 +259,8 @@ import MVMCore } } + + //------------------------------------------------------ // MARK: - Methods //------------------------------------------------------ @@ -290,10 +340,10 @@ import MVMCore standardFontSize = 0 } } - - //------------------------------------------------------ - // MARK: - Atomization - //------------------------------------------------------ +} + +// MARK: - Atomization +extension Label { public func reset() { text = nil @@ -303,16 +353,17 @@ 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 } public func setAsMolecule() { - setContentHuggingPriority(.required, for: .vertical) + styleB2(true) } public func needsToBeConstrained() -> Bool { - return true; + return true } public func alignment() -> UIStackView.Alignment { @@ -320,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/LabelView.h b/MVMCoreUI/Atoms/Views/LabelView.h index a6b1948c..8a0eb310 100644 --- a/MVMCoreUI/Atoms/Views/LabelView.h +++ b/MVMCoreUI/Atoms/Views/LabelView.h @@ -15,9 +15,4 @@ // Customize the label. @property (nullable, weak, nonatomic) Label *label; -// Change the alignment of the label -- (void)alignLeft; -- (void)alignCenter; -- (void)alignRight; - @end diff --git a/MVMCoreUI/Atoms/Views/LabelView.m b/MVMCoreUI/Atoms/Views/LabelView.m index e0817374..d13ff39a 100644 --- a/MVMCoreUI/Atoms/Views/LabelView.m +++ b/MVMCoreUI/Atoms/Views/LabelView.m @@ -11,11 +11,6 @@ #import @interface LabelView () -// Change to customize. -@property (strong, nonatomic) NSLayoutConstraint *alignCenterPin; -@property (strong, nonatomic) NSLayoutConstraint *alignCenterLeftPin; -@property (strong, nonatomic) NSLayoutConstraint *alignCenterRightPin; - // Sets up the view. - (void)setupView; @@ -36,39 +31,7 @@ Label *label = [Label commonLabelB2:YES]; [self addSubview:label]; self.label = label; - - // Align left and right constants. - NSLayoutConstraint *leftPin = [NSLayoutConstraint constraintWithItem:label attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeLeft multiplier:1.0 constant:0]; - self.leftPin = leftPin; - leftPin.active = YES; - - NSLayoutConstraint *topPin = [NSLayoutConstraint constraintWithItem:label attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeTop multiplier:1.0 constant:0]; - topPin.priority = 999; - self.topPin = topPin; - topPin.active = YES; - - NSLayoutConstraint *bottomPin = [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:label attribute:NSLayoutAttributeBottom multiplier:1.0 constant:0]; - self.bottomPin = bottomPin; - bottomPin.active = YES; - - NSLayoutConstraint *rightPin = [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:label attribute:NSLayoutAttributeRight multiplier:1.0 constant:0]; - rightPin.priority = 999; - self.rightPin = rightPin; - rightPin.active = YES; - - // Center alignments - NSLayoutConstraint *alignCenter = [NSLayoutConstraint constraintWithItem:label attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeCenterX multiplier:1.0 constant:0]; - self.alignCenterPin = alignCenter; - alignCenter.active = YES; - - NSLayoutConstraint *centerLeftPin = [NSLayoutConstraint constraintWithItem:label attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationGreaterThanOrEqual toItem:self attribute:NSLayoutAttributeLeft multiplier:1.0 constant:0]; - self.alignCenterLeftPin = centerLeftPin; - centerLeftPin.active = YES; - - NSLayoutConstraint *centerRightPin = [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationGreaterThanOrEqual toItem:label attribute:NSLayoutAttributeRight multiplier:1.0 constant:0]; - self.alignCenterRightPin = centerRightPin; - centerRightPin.active = YES; - + [self pinViewToSuperView:label]; [self alignLeft]; } } @@ -79,42 +42,20 @@ } - (void)alignLeft { - self.alignCenterPin.active = NO; - self.alignCenterLeftPin.active = NO; - self.alignCenterRightPin.active = NO; - self.leftPin.active = YES; - self.rightPin.active = YES; + [super alignLeft]; self.label.textAlignment = NSTextAlignmentLeft; } -- (void)alignCenter { - self.alignCenterPin.active = YES; - self.alignCenterLeftPin.active = YES; - self.alignCenterRightPin.active = YES; - self.leftPin.active = NO; - self.rightPin.active = NO; +- (void)alignCenterHorizontal { + [super alignCenterHorizontal]; self.label.textAlignment = NSTextAlignmentCenter; } - (void)alignRight { - self.alignCenterPin.active = NO; - self.alignCenterLeftPin.active = NO; - self.alignCenterRightPin.active = NO; - self.leftPin.active = YES; - self.rightPin.active = YES; + [super alignRight]; self.label.textAlignment = NSTextAlignmentRight; } -- (void)setLeftPinConstant:(CGFloat)constant { - [super setLeftPinConstant:constant]; - self.alignCenterLeftPin.constant = constant; -} - -- (void)setRightPinConstant:(CGFloat)constant { - [super setRightPinConstant:constant]; - self.alignCenterRightPin.constant = constant; -} - - (void)resetConstraints { [super resetConstraints]; [self alignLeft]; 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/MVMCoreUITextFieldView.h b/MVMCoreUI/Atoms/Views/MVMCoreUITextFieldView.h index 757b937e..a70804b6 100644 --- a/MVMCoreUI/Atoms/Views/MVMCoreUITextFieldView.h +++ b/MVMCoreUI/Atoms/Views/MVMCoreUITextFieldView.h @@ -15,9 +15,4 @@ + (nullable instancetype)createWithDelegate:(nullable id )delegate; -// Change the alignment of the label -- (void)alignLeft; -- (void)alignCenter; -- (void)alignRight; - @end diff --git a/MVMCoreUI/Atoms/Views/MVMCoreUITextFieldView.m b/MVMCoreUI/Atoms/Views/MVMCoreUITextFieldView.m index 4e3b2f72..f43b6356 100644 --- a/MVMCoreUI/Atoms/Views/MVMCoreUITextFieldView.m +++ b/MVMCoreUI/Atoms/Views/MVMCoreUITextFieldView.m @@ -10,9 +10,6 @@ #import "NSLayoutConstraint+MFConvenience.h" @interface MVMCoreUITextFieldView () -@property (weak, nonatomic) NSLayoutConstraint *alignCenterPin; -@property (weak, nonatomic) NSLayoutConstraint *alignCenterLeftPin; -@property (weak, nonatomic) NSLayoutConstraint *alignCenterRightPin; @end @implementation MVMCoreUITextFieldView @@ -31,74 +28,11 @@ mfTextField.translatesAutoresizingMaskIntoConstraints = NO; [self addSubview:mfTextField]; self.mvmTextField = mfTextField; - - NSLayoutConstraint *leftPin = [NSLayoutConstraint constraintWithItem:mfTextField attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeLeft multiplier:1.0 constant:0]; - self.leftPin = leftPin; - leftPin.active = YES; - - NSLayoutConstraint *topPin = [NSLayoutConstraint constraintWithItem:mfTextField attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeTop multiplier:1.0 constant:0]; - self.topPin = topPin; - topPin.active = YES; - - NSLayoutConstraint *bottomPin = [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:mfTextField attribute:NSLayoutAttributeBottom multiplier:1.0 constant:0]; - self.bottomPin = bottomPin; - bottomPin.active = YES; - - NSLayoutConstraint *rightPin = [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:mfTextField attribute:NSLayoutAttributeRight multiplier:1.0 constant:0]; - self.rightPin = rightPin; - rightPin.active = YES; - - // Center alignments - NSLayoutConstraint *alignCenter = [NSLayoutConstraint constraintWithItem:mfTextField attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeCenterX multiplier:1.0 constant:0]; - self.alignCenterPin = alignCenter; - alignCenter.active = YES; - - NSLayoutConstraint *centerLeftPin = [NSLayoutConstraint constraintWithItem:mfTextField attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationGreaterThanOrEqual toItem:self attribute:NSLayoutAttributeLeft multiplier:1.0 constant:0]; - self.alignCenterLeftPin = centerLeftPin; - centerLeftPin.active = YES; - - NSLayoutConstraint *centerRightPin = [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationGreaterThanOrEqual toItem:mfTextField attribute:NSLayoutAttributeRight multiplier:1.0 constant:0]; - self.alignCenterRightPin = centerRightPin; - centerRightPin.active = YES; - + [self pinViewToSuperView:mfTextField]; [self alignLeft]; } } -- (void)alignLeft { - self.alignCenterPin.active = NO; - self.alignCenterLeftPin.active = NO; - self.alignCenterRightPin.active = NO; - self.leftPin.active = YES; - self.rightPin.active = YES; -} - -- (void)alignCenter { - self.alignCenterPin.active = YES; - self.alignCenterLeftPin.active = YES; - self.alignCenterRightPin.active = YES; - self.leftPin.active = NO; - self.rightPin.active = NO; -} - -- (void)alignRight { - self.alignCenterPin.active = NO; - self.alignCenterLeftPin.active = NO; - self.alignCenterRightPin.active = NO; - self.leftPin.active = YES; - self.rightPin.active = YES; -} - -- (void)setLeftPinConstant:(CGFloat)constant { - self.leftPin.constant = constant; - self.alignCenterLeftPin.constant = constant; -} - -- (void)setRightPinConstant:(CGFloat)constant { - self.rightPin.constant = constant; - self.alignCenterRightPin.constant = constant; -} - - (void)resetConstraints { [super resetConstraints]; [self alignLeft]; diff --git a/MVMCoreUI/Atoms/Views/TextButtonView.h b/MVMCoreUI/Atoms/Views/TextButtonView.h index 0d4a6995..49d0c9a0 100644 --- a/MVMCoreUI/Atoms/Views/TextButtonView.h +++ b/MVMCoreUI/Atoms/Views/TextButtonView.h @@ -20,11 +20,6 @@ // inits with two buttons. - (nullable instancetype)initWithTwoButtons; -// Change the alignment of the button. Default is left. -- (void)alignLeft; -- (void)alignCenter; -- (void)alignRight; - // To add dotted underline below text button, in case of one button - (void)addDotLineBelowButton; diff --git a/MVMCoreUI/Atoms/Views/TextButtonView.m b/MVMCoreUI/Atoms/Views/TextButtonView.m index 7c9f5e23..9e6435e0 100644 --- a/MVMCoreUI/Atoms/Views/TextButtonView.m +++ b/MVMCoreUI/Atoms/Views/TextButtonView.m @@ -14,10 +14,6 @@ #import @interface TextButtonView () - -@property (weak, nonatomic) NSLayoutConstraint *alignCenterPin; -@property (weak, nonatomic) NSLayoutConstraint *alignCenterLeftPin; -@property (weak, nonatomic) NSLayoutConstraint *alignCenterRightPin; @property (strong, nonatomic) UIView *dotLine; // Sets up the view. @@ -78,37 +74,7 @@ button.translatesAutoresizingMaskIntoConstraints = NO; [self addSubview:button]; self.textButton = button; - - // Align left and right constants. - NSLayoutConstraint *leftPin = [NSLayoutConstraint constraintWithItem:button attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeLeft multiplier:1.0 constant:0]; - self.leftPin = leftPin; - leftPin.active = YES; - - NSLayoutConstraint *topPin = [NSLayoutConstraint constraintWithItem:button attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeTop multiplier:1.0 constant:0]; - self.topPin = topPin; - topPin.active = YES; - - NSLayoutConstraint *bottomPin = [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:button attribute:NSLayoutAttributeBottom multiplier:1.0 constant:0]; - self.bottomPin = bottomPin; - bottomPin.active = YES; - - NSLayoutConstraint *rightPin = [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:button attribute:NSLayoutAttributeRight multiplier:1.0 constant:0]; - self.rightPin = rightPin; - rightPin.active = YES; - - // Center alignments - NSLayoutConstraint *alignCenter = [NSLayoutConstraint constraintWithItem:button attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeCenterX multiplier:1.0 constant:0]; - self.alignCenterPin = alignCenter; - alignCenter.active = YES; - - NSLayoutConstraint *centerLeftPin = [NSLayoutConstraint constraintWithItem:button attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationGreaterThanOrEqual toItem:self attribute:NSLayoutAttributeLeft multiplier:1.0 constant:0]; - self.alignCenterLeftPin = centerLeftPin; - centerLeftPin.active = YES; - - NSLayoutConstraint *centerRightPin = [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationGreaterThanOrEqual toItem:button attribute:NSLayoutAttributeRight multiplier:1.0 constant:0]; - self.alignCenterRightPin = centerRightPin; - centerRightPin.active = YES; - + [self pinViewToSuperView:button]; [self alignLeft]; } } @@ -154,40 +120,6 @@ return self; } -- (void)alignLeft { - self.alignCenterPin.active = NO; - self.alignCenterLeftPin.active = NO; - self.alignCenterRightPin.active = YES; - self.leftPin.active = YES; - self.rightPin.active = NO; -} - -- (void)alignCenter { - self.alignCenterPin.active = YES; - self.alignCenterLeftPin.active = YES; - self.alignCenterRightPin.active = YES; - self.leftPin.active = NO; - self.rightPin.active = NO; -} - -- (void)alignRight { - self.alignCenterPin.active = NO; - self.alignCenterLeftPin.active = YES; - self.alignCenterRightPin.active = NO; - self.leftPin.active = NO; - self.rightPin.active = YES; -} - -- (void)setLeftPinConstant:(CGFloat)constant { - [super setLeftPinConstant:constant]; - self.alignCenterLeftPin.constant = constant; -} - -- (void)setRightPinConstant:(CGFloat)constant { - [super setRightPinConstant:constant]; - self.alignCenterRightPin.constant = constant; -} - - (void)resetConstraints { [super resetConstraints]; [self alignLeft]; diff --git a/MVMCoreUI/Atoms/Views/ViewConstrainingView.h b/MVMCoreUI/Atoms/Views/ViewConstrainingView.h index 6b97f891..371e137b 100644 --- a/MVMCoreUI/Atoms/Views/ViewConstrainingView.h +++ b/MVMCoreUI/Atoms/Views/ViewConstrainingView.h @@ -17,9 +17,24 @@ @property (nullable, strong, nonatomic) NSLayoutConstraint *topPin; @property (nullable, strong, nonatomic) NSLayoutConstraint *bottomPin; +@property (nullable, strong, nonatomic) NSLayoutConstraint *alignCenterPin; +@property (nullable, strong, nonatomic) NSLayoutConstraint *alignCenterLeftPin; +@property (nullable, strong, nonatomic) NSLayoutConstraint *alignCenterRightPin; + +@property (nullable, strong, nonatomic) NSLayoutConstraint *alignCenterVerticalPin; +@property (nullable, strong, nonatomic) NSLayoutConstraint *alignCenterTopPin; +@property (nullable, strong, nonatomic) NSLayoutConstraint *alignCenterBottomPin; + +@property (nullable, strong, nonatomic) NSLayoutConstraint *leftPinLow; +@property (nullable, strong, nonatomic) NSLayoutConstraint *rightPinLow; +@property (nullable, strong, nonatomic) NSLayoutConstraint *topPinLow; +@property (nullable, strong, nonatomic) NSLayoutConstraint *bottomPinLow; + + // In updateView, will set horizontal padding to default if set to YES. @property (nonatomic) BOOL updateViewHorizontalDefaults; + // Returns an empty view + (nonnull ViewConstrainingView *)emptyView; @@ -43,6 +58,7 @@ // Pins all edges to its super. 0 constant - (void)pinToSuperView; +- (void)pinViewToSuperView:(nonnull UIView *)view; // Resets all the constraints to default. - (void)resetConstraints; @@ -53,4 +69,17 @@ // Add a view to be constrained in this view. - (void)addConstrainedView:(nonnull UIView *)view; +// Change the alignment of the label +- (void)alignLeft; +- (void)alignCenterHorizontal; +- (void)alignRight; +- (void)alignFillHorizontal; +- (void)alignTop; +- (void)alignCenterVertical; +- (void)alignBottom; +- (void)alignFillVertical; + +/// Convenience function for getting the alignment from a map. ++ (UIStackViewAlignment)getAlignmentForString:(nullable NSString *)alignmentString defaultAlignment:(UIStackViewAlignment)defaultAlignment; + @end diff --git a/MVMCoreUI/Atoms/Views/ViewConstrainingView.m b/MVMCoreUI/Atoms/Views/ViewConstrainingView.m index 2830f19b..02d965b7 100644 --- a/MVMCoreUI/Atoms/Views/ViewConstrainingView.m +++ b/MVMCoreUI/Atoms/Views/ViewConstrainingView.m @@ -45,12 +45,42 @@ return constrainingView; } -- (void)pinToSuperView { - NSDictionary *dictionary = [NSLayoutConstraint constraintPinSubviewToSuperview:self]; +#pragma mark - Constraining + +- (void)pinViewToSuperView:(nonnull UIView *)view { + NSDictionary *dictionary = [NSLayoutConstraint constraintPinSubviewToSuperview:view]; self.leftPin = dictionary[ConstraintLeading]; self.topPin = dictionary[ConstraintTop]; self.bottomPin = dictionary[ConstraintBot]; self.rightPin = dictionary[ConstraintTrailing]; + + self.alignCenterPin = [view.centerXAnchor constraintEqualToAnchor:view.superview.centerXAnchor]; + self.alignCenterLeftPin = [view.leftAnchor constraintGreaterThanOrEqualToAnchor:view.superview.leftAnchor]; + self.alignCenterRightPin = [view.superview.rightAnchor constraintGreaterThanOrEqualToAnchor:view.rightAnchor]; + + self.alignCenterVerticalPin = [view.centerYAnchor constraintEqualToAnchor:view.superview.centerYAnchor]; + self.alignCenterTopPin = [view.topAnchor constraintGreaterThanOrEqualToAnchor:view.superview.topAnchor]; + self.alignCenterBottomPin = [view.superview.bottomAnchor constraintGreaterThanOrEqualToAnchor:view.bottomAnchor]; + + self.leftPinLow = [view.leftAnchor constraintEqualToAnchor:view.superview.leftAnchor]; + self.leftPinLow.priority = 200; + self.leftPinLow.active = YES; + + self.topPinLow = [view.topAnchor constraintEqualToAnchor:view.superview.topAnchor]; + self.topPinLow.priority = 200; + self.topPinLow.active = YES; + + self.rightPinLow = [view.superview.rightAnchor constraintEqualToAnchor:view.superview.rightAnchor]; + self.rightPinLow.priority = 200; + self.rightPinLow.active = YES; + + self.bottomPinLow = [view.superview.bottomAnchor constraintEqualToAnchor:view.superview.bottomAnchor]; + self.bottomPinLow.priority = 200; + self.bottomPinLow.active = YES; +} + +- (void)pinToSuperView { + [self pinViewToSuperView:self]; } - (void)setPinConstantsWithInsets:(UIEdgeInsets)insets { @@ -66,18 +96,26 @@ - (void)setLeftPinConstant:(CGFloat)constant { self.leftPin.constant = constant; + self.alignCenterLeftPin.constant = constant; + self.leftPinLow.constant = constant; } - (void)setRightPinConstant:(CGFloat)constant { self.rightPin.constant = constant; + self.alignCenterRightPin.constant = constant; + self.rightPinLow.constant = constant; } - (void)setTopPinConstant:(CGFloat)constant { self.topPin.constant = constant; + self.alignCenterTopPin.constant = constant; + self.topPinLow.constant = constant; } - (void)setBottomPinConstant:(CGFloat)constant { self.bottomPin.constant = constant; + self.alignCenterBottomPin.constant = constant; + self.bottomPinLow.constant = constant; } - (void)show { @@ -100,34 +138,147 @@ view.translatesAutoresizingMaskIntoConstraints = NO; [self addSubview:view]; self.constrainedView = view; - - NSLayoutRelation leftRelation; - if (alignment == UIStackViewAlignmentFill || alignment == UIStackViewAlignmentLeading || alignment == UIStackViewAlignmentFirstBaseline) { - leftRelation = NSLayoutRelationEqual; - } else { - leftRelation = NSLayoutRelationGreaterThanOrEqual; - } - NSLayoutRelation rightRelation; - if (alignment == UIStackViewAlignmentFill || alignment == UIStackViewAlignmentTrailing || alignment == UIStackViewAlignmentLastBaseline) { - rightRelation = NSLayoutRelationEqual; - } else { - rightRelation = NSLayoutRelationGreaterThanOrEqual; - } - NSDictionary *dictionary = [NSLayoutConstraint constraintPinSubview:view topRelation:NSLayoutRelationEqual bottomRelation:NSLayoutRelationEqual leftRelation:leftRelation rightRelation:rightRelation]; - self.leftPin = dictionary[ConstraintLeading]; - self.topPin = dictionary[ConstraintTop]; - self.bottomPin = dictionary[ConstraintBot]; - self.rightPin = dictionary[ConstraintTrailing]; + [self pinViewToSuperView:view]; + [self alignHorizontal:alignment]; } - (void)addConstrainedView:(nonnull UIView *)view { [self addConstrainedView:view alignment:UIStackViewAlignmentFill]; } +- (void)alignLeft { + self.alignCenterPin.active = NO; + self.alignCenterLeftPin.active = NO; + self.alignCenterRightPin.active = YES; + self.leftPin.active = YES; + self.rightPin.active = NO; +} + +- (void)alignCenterHorizontal { + self.alignCenterPin.active = YES; + self.alignCenterLeftPin.active = YES; + self.alignCenterRightPin.active = YES; + self.leftPin.active = NO; + self.rightPin.active = NO; +} + +- (void)alignRight { + self.alignCenterPin.active = NO; + self.alignCenterLeftPin.active = YES; + self.alignCenterRightPin.active = NO; + self.leftPin.active = NO; + self.rightPin.active = YES; +} + +- (void)alignFillHorizontal { + self.alignCenterPin.active = NO; + self.alignCenterLeftPin.active = NO; + self.alignCenterRightPin.active = NO; + self.leftPin.active = YES; + self.rightPin.active = YES; +} + +- (void)alignTop { + self.alignCenterVerticalPin.active = NO; + self.alignCenterTopPin.active = NO; + self.alignCenterBottomPin.active = YES; + self.topPin.active = YES; + self.bottomPin.active = NO; +} + +- (void)alignCenterVertical { + self.alignCenterVerticalPin.active = YES; + self.alignCenterTopPin.active = YES; + self.alignCenterBottomPin.active = YES; + self.topPin.active = NO; + self.bottomPin.active = NO; +} + +- (void)alignBottom { + self.alignCenterVerticalPin.active = NO; + self.alignCenterTopPin.active = YES; + self.alignCenterBottomPin.active = NO; + self.topPin.active = NO; + self.bottomPin.active = YES; +} + +- (void)alignFillVertical { + self.alignCenterVerticalPin.active = NO; + self.alignCenterTopPin.active = NO; + self.alignCenterBottomPin.active = NO; + self.topPin.active = YES; + self.bottomPin.active = YES; +} + ++ (UIStackViewAlignment)getAlignmentForString:(nullable NSString *)alignmentString defaultAlignment:(UIStackViewAlignment)defaultAlignment { + if ([alignmentString isEqualToString:@"leading"]) { + return UIStackViewAlignmentLeading; + } else if ([alignmentString isEqualToString:@"trailing"]) { + return UIStackViewAlignmentTrailing; + } else if ([alignmentString isEqualToString:@"center"]) { + return UIStackViewAlignmentCenter; + } else if ([alignmentString isEqualToString:@"fill"]) { + return UIStackViewAlignmentFill; + } else { + return defaultAlignment; + } +} + +#pragma mark - MVMCoreUIViewConstrainingProtocol + +- (void)alignHorizontal:(UIStackViewAlignment)alignment { + switch (alignment) { + case UIStackViewAlignmentCenter: + [self alignCenterHorizontal]; + break; + case UIStackViewAlignmentLeading: + [self alignLeft]; + break; + case UIStackViewAlignmentTrailing: + [self alignRight]; + break; + case UIStackViewAlignmentFill: + [self alignFillHorizontal]; + break; + default: + break; + } +} + +- (void)alignVertical:(UIStackViewAlignment)alignment { + switch (alignment) { + case UIStackViewAlignmentCenter: + [self alignCenterVertical]; + break; + case UIStackViewAlignmentLeading: + [self alignTop]; + break; + case UIStackViewAlignmentTrailing: + [self alignBottom]; + break; + case UIStackViewAlignmentFill: + [self alignFillVertical]; + break; + default: + break; + } +} + +- (void)shouldSetHorizontalMargins:(BOOL)shouldSet { + self.updateViewHorizontalDefaults = shouldSet; +} + +#pragma mark - MVMCoreViewProtocol + - (void)setupView { [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 { @@ -145,14 +296,14 @@ } } +#pragma mark - MVMCoreUIMoleculeViewProtocol + - (void)reset { if ([self.constrainedView respondsToSelector:@selector(reset)]) { [self.constrainedView performSelector:@selector(reset)]; } } -#pragma mark - MVMCoreUIMoleculeViewProtocol - - (void)setAsMolecule { self.updateViewHorizontalDefaults = YES; } diff --git a/MVMCoreUI/Categories/NSLayoutConstraint+MFConvenience.h b/MVMCoreUI/Categories/NSLayoutConstraint+MFConvenience.h index 0d0d281c..3d615da7 100644 --- a/MVMCoreUI/Categories/NSLayoutConstraint+MFConvenience.h +++ b/MVMCoreUI/Categories/NSLayoutConstraint+MFConvenience.h @@ -55,7 +55,7 @@ extern NSString * _Nonnull const ConstraintWidth; + (nullable NSDictionary *)constraintPinView:(nonnull UIView*)view heightConstraint:(BOOL)pinHeight heightConstant:(CGFloat)heightConstant widthConstraint:(BOOL)pinWidth widthConstant:(CGFloat)widthConstant; // pins 2 views in same hierarchy -+(nullable NSLayoutConstraint *)constraintPinFirstView :(nonnull UIView*)firstView toSecondView :(nonnull UIView*)secondView withConstant :(CGFloat)constant directionVertical :(BOOL)directionVertical; ++(nonnull NSLayoutConstraint *)constraintPinFirstView :(nonnull UIView*)firstView toSecondView :(nonnull UIView*)secondView withConstant :(CGFloat)constant directionVertical :(BOOL)directionVertical; +(nullable NSDictionary *)constraintAlignView :(nonnull UIView *)firstView toSecondView :(nonnull UIView *)secondView alignX :(BOOL)alignX alignY :(BOOL)alignY; diff --git a/MVMCoreUI/Molecules/ButtonView.swift b/MVMCoreUI/Molecules/ButtonView.swift index 3e863a8e..4596164c 100644 --- a/MVMCoreUI/Molecules/ButtonView.swift +++ b/MVMCoreUI/Molecules/ButtonView.swift @@ -10,9 +10,6 @@ import UIKit @objcMembers open class ButtonView: ViewConstrainingView { open var primaryButton: PrimaryButton? = PrimaryButton.button() - open var alignCenterPin: NSLayoutConstraint? - open var alignCenterLeftPin: NSLayoutConstraint? - open var alignCenterRightPin: NSLayoutConstraint? // MARK: - Inits public init() { @@ -53,7 +50,7 @@ import UIKit open override func setupView() { super.setupView() setupButton() - alignCenter() + alignCenterHorizontal() } // MARK: - MVMCoreUIMoleculeViewProtocol @@ -75,56 +72,9 @@ import UIKit func setupButton() { if let primaryButton = primaryButton, !subviews.contains(primaryButton) { addSubview(primaryButton) - setupConstraints(forView: primaryButton) + pinView(toSuperView: primaryButton) } } - - func setupConstraints(forView view: UIView) { - leftPin = view.leftAnchor.constraint(equalTo: leftAnchor) - topPin = view.topAnchor.constraint(equalTo: topAnchor) - rightPin = rightAnchor.constraint(equalTo: view.rightAnchor) - bottomPin = bottomAnchor.constraint(equalTo: view.bottomAnchor) - leftPin?.isActive = true - topPin?.isActive = true - rightPin?.isActive = true - bottomPin?.isActive = true - - alignCenterPin = view.centerXAnchor.constraint(equalTo: centerXAnchor) - alignCenterLeftPin = view.leftAnchor.constraint(greaterThanOrEqualTo: leftAnchor) - alignCenterRightPin = rightAnchor.constraint(greaterThanOrEqualTo: view.rightAnchor) - } - - open func alignLeft() { - alignCenterPin?.isActive = false - alignCenterLeftPin?.isActive = false - alignCenterRightPin?.isActive = true - leftPin?.isActive = true - rightPin?.isActive = false - } - - open func alignCenter() { - alignCenterPin?.isActive = true - alignCenterLeftPin?.isActive = true - alignCenterRightPin?.isActive = true - leftPin?.isActive = false - rightPin?.isActive = false - } - - open func alignRight() { - alignCenterPin?.isActive = false - alignCenterLeftPin?.isActive = true - alignCenterRightPin?.isActive = false - leftPin?.isActive = false - rightPin?.isActive = true - } - - open func alignFill() { - alignCenterPin?.isActive = false - alignCenterLeftPin?.isActive = false - alignCenterRightPin?.isActive = false - leftPin?.isActive = true - rightPin?.isActive = true - } open override func setLeftPinConstant(_ constant: CGFloat) { super.setLeftPinConstant(constant) diff --git a/MVMCoreUI/Molecules/MVMCoreUIMoleculeViewProtocol.h b/MVMCoreUI/Molecules/MVMCoreUIMoleculeViewProtocol.h index 962b0c0c..9140245e 100644 --- a/MVMCoreUI/Molecules/MVMCoreUIMoleculeViewProtocol.h +++ b/MVMCoreUI/Molecules/MVMCoreUIMoleculeViewProtocol.h @@ -27,6 +27,9 @@ /// For the molecule list to load more efficiently. + (CGFloat)estimatedHeightForRow:(nullable NSDictionary *)json; +/// Allows the molecule to set its name for reuse. Default could be moleculeName. ++ (nullable NSString *)nameForReuse:(nullable NSDictionary *)molecule delegateObject:(nullable MVMCoreUIDelegateObject *)delegateObject; + @end diff --git a/MVMCoreUI/Molecules/MoleculeStackView.swift b/MVMCoreUI/Molecules/MoleculeStackView.swift index 5f7f8957..7c72cf09 100644 --- a/MVMCoreUI/Molecules/MoleculeStackView.swift +++ b/MVMCoreUI/Molecules/MoleculeStackView.swift @@ -8,10 +8,82 @@ import UIKit -public class MoleculeStackView: MFView { - var spacingBlock: ((Any) -> UIEdgeInsets)? - var moleculesArray: [UIView]? - var useMargins: Bool = false +public class StackItem { + var view: UIView + var spacing: CGFloat? + var percentage: Int? + var verticalAlignment: UIStackView.Alignment? + var horizontalAlignment: UIStackView.Alignment? + + init(with view: UIView) { + self.view = view + } + + init(with view: UIView, json: [AnyHashable: Any]) { + self.view = view + update(with: json) + } + + func update(with json: [AnyHashable: Any]) { + spacing = json.optionalCGFloatForKey("spacing") + percentage = json["percent"] as? Int + if let alignment = json.optionalStringForKey("verticalAlignment") { + verticalAlignment = ViewConstrainingView.getAlignmentFor(alignment, defaultAlignment: .fill) + } + if let alignment = json.optionalStringForKey("horizontalAlignment") { + horizontalAlignment = ViewConstrainingView.getAlignmentFor(alignment, defaultAlignment: .fill) + } + } +} + +public class MoleculeStackView: ViewConstrainingView { + var contentView: UIView = MVMCoreUICommonViewsUtility.commonView() + var items: [StackItem] = [] + + /// For setting the direction of the stack + var axis: NSLayoutConstraint.Axis = .vertical { + didSet { + updateViewHorizontalDefaults = axis == .horizontal + if axis != oldValue { + restack() + } + } + } + + /// The spacing to use between each item in the stack. + var spacing: CGFloat = 16 { + didSet { + if spacing != oldValue { + restack() + } + } + } + + // MARK: - Helpers + public func setAxisWithJSON(_ json: [AnyHashable: Any]?) { + switch json?.optionalStringForKey("axis") { + case "horizontal": + axis = .horizontal + default: + axis = .vertical + } + } + + public func pinView(_ view: UIView, toView: UIView, attribute: NSLayoutConstraint.Attribute, relation: NSLayoutConstraint.Relation, priority: UILayoutPriority, constant: CGFloat) { + let constraint = NSLayoutConstraint(item: view, attribute: attribute, relatedBy: relation, toItem: toView, attribute: attribute, multiplier: 1.0, constant: constant) + constraint.priority = priority + constraint.isActive = true + } + + /// Restacks the existing items. + func restack() { + MVMCoreUIStackableViewController.remove(contentView.subviews) + let stackItems = items + items.removeAll() + for (index, item) in stackItems.enumerated() { + addStackItem(item, lastItem: index == stackItems.count - 1) + } + } // MARK: - Inits public override init(frame: CGRect) { @@ -23,11 +95,6 @@ public class MoleculeStackView: MFView { setWithJSON(json, delegateObject: delegateObject, additionalData: additionalData) } - public convenience init(withJSON json: [AnyHashable: Any]?, delegateObject: MVMCoreUIDelegateObject?, spacingBlock: ((Any) -> UIEdgeInsets)?) { - self.init(withJSON: json, delegateObject: delegateObject, additionalData: nil) - self.spacingBlock = spacingBlock - } - public required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -35,47 +102,153 @@ public class MoleculeStackView: MFView { // MARK: - MFViewProtocol public override func setupView() { super.setupView() + guard contentView.superview == nil else { + return + } translatesAutoresizingMaskIntoConstraints = false backgroundColor = .clear - self.setContentHuggingPriority(UILayoutPriority.required, for: NSLayoutConstraint.Axis.vertical) - self.setContentCompressionResistancePriority(UILayoutPriority.required, for: NSLayoutConstraint.Axis.vertical) + addConstrainedView(contentView) + contentView.setContentHuggingPriority(.defaultHigh, for: .vertical) + contentView.setContentHuggingPriority(.defaultHigh, for: .horizontal) } public override func updateView(_ size: CGFloat) { super.updateView(size) - for view in subviews { - if let mvmView = view as? MVMCoreViewProtocol { - mvmView.updateView(size) - } + for item in items { + (item.view as? MVMCoreViewProtocol)?.updateView(size) } } // MARK: - MVMCoreUIMoleculeViewProtocol + public override func setAsMolecule() { + updateViewHorizontalDefaults = false + } + + public override func reset() { + backgroundColor = .clear + for item in items { + if let view = item.view as? MVMCoreUIMoleculeViewProtocol { + view.reset?() + } + } + } + open override func setWithJSON(_ json: [AnyHashable: Any]?, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) { + let previousJSON = self.json super.setWithJSON(json, delegateObject: delegateObject, additionalData: additionalData) + MVMCoreUIStackableViewController.remove(contentView.subviews) + + // If the items in the stack are the same, just update previous items instead of re-allocating. + var items: [StackItem]? + if MoleculeStackView.name(forReuse: previousJSON, delegateObject: delegateObject) == MoleculeStackView.name(forReuse: json, delegateObject: delegateObject) { + items = self.items + } + self.items = [] + + if let colorString = json?.optionalStringForKey(KeyBackgroundColor) { + backgroundColor = .mfGet(forHex: colorString) + } + guard let molecules = json?.arrayForKey(KeyMolecules) as? [[String: Any]] else { return } - // Create the molecules and set the json. - var moleculesArray = [] as [UIView] - for moleculeJSON in molecules { - if let molecule = MVMCoreUIMoleculeMappingObject.shared()?.createMolecule(forJSON: moleculeJSON, delegateObject: delegateObject, constrainIfNeeded: true) { - moleculesArray.append(molecule) - } - } + // Sets the stack attributes + setAxisWithJSON(json) + spacing = json?.optionalCGFloatForKey("spacing") ?? 16 - guard moleculesArray.count > 0 else { - return - } - - if let spacingBlock = spacingBlock { - MVMCoreUIStackableViewController.populateView(self, withUIArray: moleculesArray, useMargins: useMargins, withSpacingBlock: spacingBlock) + // Set the alignment for the stack in the containing view. The json driven value is for the axis direction alignment. + if axis == .vertical { + alignHorizontal(.fill) + alignVertical(ViewConstrainingView.getAlignmentFor(json?.optionalStringForKey("alignment"), defaultAlignment: .fill)) } else { - let separation = json?.optionalCGFloatForKey("separation") ?? PaddingDefault - MVMCoreUIStackableViewController.populateView(self, withUIArray: moleculesArray, useMargins: useMargins) { (object) -> UIEdgeInsets in - return UIEdgeInsets.init(top: separation, left: 0, bottom: 0, right: 0) + alignHorizontal(ViewConstrainingView.getAlignmentFor(json?.optionalStringForKey("alignment"), defaultAlignment: .fill)) + alignVertical(.leading) + } + + // Adds the molecules and sets the json. + for (index, map) in molecules.enumerated() { + if let moleculeJSON = map.optionalDictionaryForKey(KeyMolecule) { + var view: UIView? + if let item = items?[index] { + (item.view as? MVMCoreUIMoleculeViewProtocol)?.setWithJSON(moleculeJSON, delegateObject: delegateObject, additionalData: additionalData) + item.update(with: moleculeJSON) + view = item.view + addStackItem(item, lastItem: index == molecules.count - 1) + } else if let molecule = MVMCoreUIMoleculeMappingObject.shared()?.createMolecule(forJSON: moleculeJSON, delegateObject: delegateObject, constrainIfNeeded: true) { + view = molecule + addStackItem(StackItem(with: molecule, json: map), lastItem: index == molecules.count - 1) + } + + (view as? MVMCoreUIViewConstrainingProtocol)?.shouldSetHorizontalMargins?(axis == .vertical) + (view as? MVMCoreUIViewConstrainingProtocol)?.shouldSetVerticalMargins?(false) } } } + + public override static func name(forReuse molecule: [AnyHashable : Any]?, delegateObject: MVMCoreUIDelegateObject?) -> String? { + // This will aggregate names of molecules to make an id. + var name = "" + guard let molecules = molecule?.optionalArrayForKey(KeyMolecules) else { + return name + } + for case let item as [AnyHashable: AnyHashable] in molecules { + if let moleculeName = item.stringOptionalWithChainOfKeysOrIndexes([KeyMolecule, KeyMoleculeName]) { + name.append(moleculeName) + } + } + return name + } + + // MARK: - Adding to stack + /// Adds the view to the stack. + func addView(_ view: UIView, lastItem: Bool) { + addStackItem(StackItem(with: view), lastItem: lastItem) + } + + /// Adds the stack item to the stack. + func addStackItem(_ stackItem: StackItem, lastItem: Bool) { + let view = stackItem.view + contentView.addSubview(view) + view.translatesAutoresizingMaskIntoConstraints = false + + let spacing = stackItem.spacing ?? self.spacing + if let view = view as? MVMCoreUIViewConstrainingProtocol { + let verticalAlignment = stackItem.verticalAlignment ?? (stackItem.percentage == nil && axis == .vertical ? .fill : (axis == .vertical ? .leading : .center)) + let horizontalAlignment = stackItem.horizontalAlignment ?? view.alignment?() ?? (axis == .vertical || stackItem.percentage == nil ? .fill : .leading) + view.alignHorizontal?(horizontalAlignment) + view.alignVertical?(verticalAlignment) + } + if axis == .vertical { + if items.count == 0 { + pinView(view, toView: contentView, attribute: .top, relation: .equal, priority: .required, constant: spacing) + } else if let previousView = items.last?.view { + _ = NSLayoutConstraint(pinFirstView: previousView, toSecondView: view, withConstant: spacing, directionVertical: true) + } + pinView(view, toView: contentView, attribute: .leading, relation: .equal, priority: .required, constant: 0) + pinView(contentView, toView: view, attribute: .trailing, relation: .equal, priority: .required, constant: 0) + if let percent = stackItem.percentage { + view.heightAnchor.constraint(equalTo: contentView.heightAnchor, multiplier: CGFloat(percent)/100.0).isActive = true + } + if lastItem { + pinView(contentView, toView: view, attribute: .bottom, relation: .equal, priority: .required, constant: 0) + } + } else { + if items.count == 0 { + // First horizontal item has no spacing by default unless told otherwise. + pinView(view, toView: contentView, attribute: .leading, relation: .equal, priority: .required, constant: stackItem.spacing ?? 0) + } else if let previousView = items.last?.view { + _ = NSLayoutConstraint(pinFirstView: previousView, toSecondView: view, withConstant: spacing, directionVertical: false) + } + pinView(view, toView: contentView, attribute: .top, relation: .equal, priority: .required, constant: 0) + pinView(contentView, toView: view, attribute: .bottom, relation: .equal, priority: .required, constant: 0) + if let percent = stackItem.percentage { + view.widthAnchor.constraint(equalTo: contentView.widthAnchor, multiplier: CGFloat(percent)/100.0).isActive = true + } + } + if lastItem { + pinView(contentView, toView: view, attribute: .right, relation: .equal, priority: .required, constant: 0) + } + items.append(stackItem) + } } diff --git a/MVMCoreUI/Molecules/MoleculeTableViewCell.swift b/MVMCoreUI/Molecules/MoleculeTableViewCell.swift index 298fa6b5..695b5849 100644 --- a/MVMCoreUI/Molecules/MoleculeTableViewCell.swift +++ b/MVMCoreUI/Molecules/MoleculeTableViewCell.swift @@ -43,11 +43,25 @@ import UIKit public func updateView(_ size: CGFloat) { MFStyler.setDefaultMarginsFor(self, size: size, horizontal: true, vertical: true) if #available(iOS 11.0, *) { - contentView.directionalLayoutMargins = directionalLayoutMargins + if accessoryView != nil { + // Smaller left margin if accessory view. + var margin = directionalLayoutMargins + margin.trailing = 16 + contentView.directionalLayoutMargins = margin + } else { + contentView.directionalLayoutMargins = directionalLayoutMargins + } topSeparatorView?.setLeftAndRightPinConstant(directionalLayoutMargins.leading) bottomSeparatorView?.setLeftAndRightPinConstant(directionalLayoutMargins.leading) } else { - contentView.layoutMargins = layoutMargins + if accessoryView != nil { + // Smaller left margin if accessory view. + var margin = layoutMargins + margin.right = 16 + contentView.layoutMargins = margin + } else { + contentView.layoutMargins = layoutMargins + } topSeparatorView?.setLeftAndRightPinConstant(layoutMargins.left) bottomSeparatorView?.setLeftAndRightPinConstant(layoutMargins.left) } @@ -67,7 +81,7 @@ import UIKit } // MARK: - MVMCoreUIMoleculeViewProtocol - public func setWithJSON(_ json: [AnyHashable : Any]?, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable : Any]?) { + public func setWithJSON(_ json: [AnyHashable: Any]?, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) { self.json = json; guard let json = json, let moleculeJSON = json.optionalDictionaryForKey(KeyMolecule) else { return @@ -75,15 +89,17 @@ import UIKit if molecule == nil { if let moleculeView = MVMCoreUIMoleculeMappingObject.shared()?.createMolecule(forJSON: moleculeJSON, delegateObject: delegateObject, constrainIfNeeded: true) { contentView.addSubview(moleculeView) - let standardConstraints = (moleculeView as? MVMCoreUIViewConstrainingProtocol)?.useStandardConstraints?() ?? true + var standardConstraints = true + if let castView = moleculeView as? MVMCoreUIViewConstrainingProtocol { + standardConstraints = castView.useStandardConstraints?() ?? true + castView.shouldSetHorizontalMargins?(!standardConstraints) + castView.shouldSetVerticalMargins?(!standardConstraints) + } NSLayoutConstraint.activate(Array(NSLayoutConstraint.pinView(toSuperview: moleculeView, useMargins: standardConstraints).values)) if standardConstraints { let constraint = contentView.heightAnchor.constraint(equalToConstant: 80) constraint.priority = .defaultLow constraint.isActive = true - if let moleculeView = moleculeView as? ViewConstrainingView { - moleculeView.updateViewHorizontalDefaults = false - } } molecule = moleculeView } @@ -110,15 +126,27 @@ import UIKit molecule?.reset?() } - public static func estimatedHeight(forRow json: [AnyHashable : Any]?) -> CGFloat { + public static func estimatedHeight(forRow json: [AnyHashable: Any]?) -> CGFloat { guard let moleculeJSON = json?.optionalDictionaryForKey(KeyMolecule), let theClass = MVMCoreUIMoleculeMappingObject.shared()?.getMoleculeClass(withJSON: moleculeJSON, delegateObject: nil), let estimatedHeightFor = theClass.estimatedHeight else { - return 0 + return 80 } return estimatedHeightFor(moleculeJSON) } + public static func name(forReuse molecule: [AnyHashable: Any]?, delegateObject: MVMCoreUIDelegateObject?) -> String? { + if let molecule = molecule?.optionalDictionaryForKey(KeyMolecule), + let moleculeName = molecule.optionalStringForKey(KeyMoleculeName), + let moleculeClass = MVMCoreUIMoleculeMappingObject.shared()?.moleculeMapping[moleculeName] as? AnyClass, + let castClass = moleculeClass as? MVMCoreUIMoleculeViewProtocol.Type, + let nameFunc = castClass.name { + return nameFunc(molecule, delegateObject) + } else { + return molecule?.optionalDictionaryForKey(KeyMolecule)?.optionalStringForKey(KeyMoleculeName) + } + } + // MARK: - Arrow /// Adds the standard mvm style caret to the accessory view public func addCaretViewAccessory() { @@ -129,16 +157,12 @@ import UIKit let height: CGFloat = 10 caretView = CaretView(lineThickness: CaretView.thin) caretView?.frame = CGRect(x: 0, y: 0, width: width, height: height) - caretViewWidthSizeObject = MFSizeObject(scalingStandardSize: width) - caretViewHeightSizeObject = MFSizeObject(scalingStandardSize: height) + caretViewWidthSizeObject = MFSizeObject(standardSize: width, standardiPadPortraitSize: 9) + caretViewHeightSizeObject = MFSizeObject(standardSize: height, standardiPadPortraitSize: 16) accessoryView = caretView } // MARK: - MoleculeListCellProtocol - public static func moleculeName(_ molecule: [AnyHashable : Any]?, delegateObject: MVMCoreUIDelegateObject?) -> String? { - return molecule?.optionalDictionaryForKey(KeyMolecule)?.optionalStringForKey(KeyMoleculeName) - } - /// For when the separator between cells shows using json and frequency. Default is type: standard, frequency: allExceptTop. public func setSeparatorWithJSON(_ json: [AnyHashable: Any]?, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?, indexPath: IndexPath) { addSeparatorsIfNeeded() @@ -155,7 +179,7 @@ import UIKit } } - public func didSelectCell(atIndex indexPath: IndexPath, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable : Any]?) { + public func didSelectCell(atIndex indexPath: IndexPath, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) { if let actionMap = json?.optionalDictionaryForKey("actionMap") { MVMCoreActionHandler.shared()?.handleAction(with: actionMap, additionalData: additionalData, delegateObject: delegateObject) } diff --git a/MVMCoreUI/Molecules/ProgressBarView.swift b/MVMCoreUI/Molecules/ProgressBarWithLabel.swift similarity index 95% rename from MVMCoreUI/Molecules/ProgressBarView.swift rename to MVMCoreUI/Molecules/ProgressBarWithLabel.swift index 8106e0f7..9740e1ba 100644 --- a/MVMCoreUI/Molecules/ProgressBarView.swift +++ b/MVMCoreUI/Molecules/ProgressBarWithLabel.swift @@ -8,7 +8,7 @@ import UIKit -@objcMembers public class ProgressBarView: ViewConstrainingView { +@objcMembers public class ProgressBarWithLabel: ViewConstrainingView { var progress = ProgressBar() var topleftlabel = Label() @@ -22,7 +22,8 @@ import UIKit open override func setWithJSON(_ json: [AnyHashable: Any]?, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) { super.setWithJSON(json, delegateObject: delegateObject, additionalData: additionalData) - progress.setWithJSON(json, delegateObject: delegateObject, additionalData: additionalData) + let progressbarjson = json?.optionalDictionaryForKey("progressbar") + progress.setWithJSON(progressbarjson, delegateObject: delegateObject, additionalData: additionalData) let topleftlabeljson = json?.optionalDictionaryForKey("label") let toprightlabeljson = json?.optionalDictionaryForKey("toprightlabel") diff --git a/MVMCoreUI/Molecules/StandardHeaderView.swift b/MVMCoreUI/Molecules/StandardHeaderView.swift index 26ce345b..2391f062 100644 --- a/MVMCoreUI/Molecules/StandardHeaderView.swift +++ b/MVMCoreUI/Molecules/StandardHeaderView.swift @@ -68,7 +68,6 @@ public class StandardHeaderView: ViewConstrainingView { if let separatorView = SeparatorView.separatorAdd(to: self, position: SeparatorPositionBot, withHorizontalPadding: 0) { separatorView.setAsHeavy() - separatorView.isHidden = true addSubview(separatorView) self.separatorView = separatorView } diff --git a/MVMCoreUI/Molecules/TwoButtonView.swift b/MVMCoreUI/Molecules/TwoButtonView.swift index 48becca2..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) } @@ -103,8 +103,8 @@ import UIKit addSubview(viewForButtons) self.viewForButtons = viewForButtons - setupConstraints(forView: viewForButtons) - alignCenter() + pinView(toSuperView: viewForButtons) + alignCenterHorizontal() createPrimaryButton() createSecondaryButton() @@ -118,8 +118,8 @@ import UIKit createPrimaryButton() if let primaryButton = primaryButton { addSubview(primaryButton) - setupConstraints(forView: primaryButton) - alignCenter() + pinView(toSuperView: primaryButton) + alignCenterHorizontal() } } diff --git a/MVMCoreUI/OtherHandlers/MVMCoreUIMoleculeMappingObject.m b/MVMCoreUI/OtherHandlers/MVMCoreUIMoleculeMappingObject.m index 12a3fcde..01321470 100644 --- a/MVMCoreUI/OtherHandlers/MVMCoreUIMoleculeMappingObject.m +++ b/MVMCoreUI/OtherHandlers/MVMCoreUIMoleculeMappingObject.m @@ -36,14 +36,15 @@ @"caretButton": CaretButton.class, @"textField" : MFTextField.class, @"checkbox" : MVMCoreUICheckBox.class, - @"progressBarView" : ProgressBarView.class, + @"progressBarWithLabel" : ProgressBarWithLabel.class, @"progressBar": ProgressBar.class, @"textField": MFTextField.class, @"checkbox": MVMCoreUICheckBox.class, @"radioButton": RadioButton.class, @"listItem": MoleculeTableViewCell.class, @"switchLineItem": SwitchLineItem.class, - @"switch": Switch.class + @"switch": Switch.class, + @"leftRightLabelView": LeftRightLabelView.class } mutableCopy]; }); return mapping; diff --git a/MVMCoreUI/OtherHandlers/MVMCoreUIViewConstrainingProtocol.h b/MVMCoreUI/OtherHandlers/MVMCoreUIViewConstrainingProtocol.h index 8bdc3eb1..42114416 100644 --- a/MVMCoreUI/OtherHandlers/MVMCoreUIViewConstrainingProtocol.h +++ b/MVMCoreUI/OtherHandlers/MVMCoreUIViewConstrainingProtocol.h @@ -21,4 +21,14 @@ /// Can be used to override any standard constraints that may be added. - (BOOL)useStandardConstraints; +/// Will align if it can. +- (void)alignHorizontal:(UIStackViewAlignment)alignment; +- (void)alignVertical:(UIStackViewAlignment)alignment; + +/// Containing Views can tell the contained if they should use horizontal margins. +- (void)shouldSetHorizontalMargins:(BOOL)shouldSet; + +/// Containing Views can tell the contained if they should use vertical margins. +- (void)shouldSetVerticalMargins:(BOOL)shouldSet; + @end diff --git a/MVMCoreUI/Styles/MFStyler.h b/MVMCoreUI/Styles/MFStyler.h index 00193633..095426ad 100644 --- a/MVMCoreUI/Styles/MFStyler.h +++ b/MVMCoreUI/Styles/MFStyler.h @@ -208,6 +208,9 @@ B3 -> Legal #pragma mark - 2.0 styles +/// Will style the label based on the string. Accepted values, H1, H2, H3, H32, B1, B2, B3, B20 ++ (void)styleLabel:(nonnull UILabel *)label withStyle:(nullable NSString *)style; + + (void)styleLabelH1:(nonnull UILabel *)label genericScaling:(BOOL)genericScaling; + (void)styleLabelH1:(nonnull UILabel *)label; @@ -253,6 +256,9 @@ B3 -> Legal #pragma mark - Attributed Strings +/// Will style the string based on the string. Accepted values, H1, H2, H3, H32, B1, B2, B3, B20 ++ (nonnull NSAttributedString *)styleGetAttributedString:(nullable NSString *)string withStyle:(nullable NSString *)style; + + (nonnull NSAttributedString *)styleGetAttributedString:(nullable NSString *)string font:(nonnull UIFont *)font color:(nonnull UIColor *)color; + (nonnull NSAttributedString *)styleGetH1AttributedString:(nullable NSString *)string; + (nonnull NSAttributedString *)styleGetH2AttributedString:(nullable NSString *)string; diff --git a/MVMCoreUI/Styles/MFStyler.m b/MVMCoreUI/Styles/MFStyler.m index b4711e1a..6d0810e2 100644 --- a/MVMCoreUI/Styles/MFStyler.m +++ b/MVMCoreUI/Styles/MFStyler.m @@ -512,6 +512,26 @@ CGFloat const LabelWithInternalButtonLineSpace = 2; #pragma mark - 2.0 Styles ++ (void)styleLabel:(nonnull UILabel *)label withStyle:(nullable NSString *)style { + if ([style isEqualToString:@"H1"]) { + [self styleLabelH1:label]; + } else if ([style isEqualToString:@"H2"]) { + [self styleLabelH2:label]; + } else if ([style isEqualToString:@"H3"]) { + [self styleLabelH3:label]; + } else if ([style isEqualToString:@"H32"]) { + [self styleLabelH32:label]; + } else if ([style isEqualToString:@"B1"]) { + [self styleLabelB1:label]; + } else if ([style isEqualToString:@"B3"]) { + [self styleLabelB3:label]; + } else if ([style isEqualToString:@"B20"]) { + [self styleLabelB20:label]; + } else { + [self styleLabelB2:label]; + } +} + + (void)styleLabelH1:(nonnull UILabel *)label genericScaling:(BOOL)genericScaling { label.font = [MFStyler fontH1:genericScaling]; label.textColor = [UIColor blackColor]; @@ -631,6 +651,26 @@ CGFloat const LabelWithInternalButtonLineSpace = 2; #pragma mark - Attributed Strings ++ (nonnull NSAttributedString *)styleGetAttributedString:(nullable NSString *)string withStyle:(nullable NSString *)style { + if ([style isEqualToString:@"H1"]) { + return [self styleGetH1AttributedString:string]; + } else if ([style isEqualToString:@"H2"]) { + return [self styleGetH2AttributedString:string]; + } else if ([style isEqualToString:@"H3"]) { + return [self styleGetH3AttributedString:string]; + } else if ([style isEqualToString:@"H32"]) { + return [self styleGetH32AttributedString:string]; + } else if ([style isEqualToString:@"B1"]) { + return [self styleGetB1AttributedString:string]; + } else if ([style isEqualToString:@"B3"]) { + return [self styleGetB3AttributedString:string]; + } else if ([style isEqualToString:@"B20"]) { + return [self styleGetB20AttributedString:string]; + } else { + return [self styleGetB2AttributedString:string]; + } +} + + (nonnull NSAttributedString *)styleGetAttributedString:(nullable NSString *)string font:(nonnull UIFont *)font color:(nonnull UIColor *)color { NSAttributedString *attributedString = nil; if (![string isEqual:[NSNull null]] && string.length > 0) { @@ -657,6 +697,10 @@ CGFloat const LabelWithInternalButtonLineSpace = 2; return [MFStyler styleGetAttributedString:string font:[MFStyler fontH3] color:[UIColor blackColor]]; } ++ (nonnull NSAttributedString *)styleGetH32AttributedString:(nullable NSString *)string { + return [MFStyler styleGetAttributedString:string font:[MFStyler fontH32] color:[UIColor blackColor]]; +} + + (nonnull NSAttributedString *)styleGetB1AttributedString:(nullable NSString *)string { return [MFStyler styleGetAttributedString:string font:[MFStyler fontB1] color:[UIColor blackColor]]; } @@ -669,6 +713,10 @@ CGFloat const LabelWithInternalButtonLineSpace = 2; return [MFStyler styleGetAttributedString:string font:[MFStyler fontB3] color:[UIColor mfBattleshipGrey]]; } ++ (nonnull NSAttributedString *)styleGetB20AttributedString:(nullable NSString *)string { + return [MFStyler styleGetAttributedString:string font:[MFStyler fontB20] color:[UIColor blackColor]]; +} + + (nonnull NSAttributedString *)styleGetDisabledB1AttributedString:(nullable NSString *)string { return [MFStyler styleGetAttributedString:string font:[MFStyler fontB1] color:[UIColor mfLighterGrayColor]]; } diff --git a/MVMCoreUI/Styles/usage.json b/MVMCoreUI/Styles/usage.json new file mode 100644 index 00000000..0f6ee862 --- /dev/null +++ b/MVMCoreUI/Styles/usage.json @@ -0,0 +1,108 @@ +{ +"pageType": "usage", +"template": "moleculeList", +"header": { + "moleculeName":"header", + "headline":{ + "moleculeName": "label", + "text":"See who's using what." +}, +"body":{ + "moleculeName": "label", + "text":"Data usage was estimated as of Jun 11." +}, +"molecules": [{ + "moleculeName": "listItem", + "molecule" : { + "moleculeName":"moleculeStack", + "axis": "horizontal", + "molecules": [{ + "percent": 60, + "molecule": { + "moleculeName": "label", + "text": "Johny Verizon\n555.555.5555\nGo Unlimited", + "attributes": [{ + "location": 0, + "length": 13, + "type": "font", + "style": "B1" + }] + } + },{ + "horizontalAlignment": "trailing", + "molecule": { + "moleculeName": "label", + "text": "4.55 GB" + } + }] + }, + "actionMap":{ + "actionType": "openPage", + "pageType": "usageDetails", + "extraParameters": { + "mdn": "5555555555" + } + }},{ + "moleculeName": "listItem", + "molecule" : { + "moleculeName":"moleculeStack", + "axis": "horizontal", + "molecules": [{ + "percent": 60, + "molecule": { + "moleculeName": "label", + "text": "Sree Verizon\n555.555.5556", + "attributes": [{ + "location": 0, + "length": 12, + "type": "font", + "style": "B1" + }] + } + },{ + "horizontalAlignment": "trailing", + "molecule": { + "moleculeName": "label", + "text": "0 GB" + } + }] + }, + "actionMap":{ + "actionType": "openPage", + "pageType": "usageDetails", + "extraParameters": { + "mdn": "5555555556" + } + }},{ + "moleculeName": "listItem", + "molecule" : { + "moleculeName":"moleculeStack", + "axis": "horizontal", + "molecules": [{ + "percent": 60, + "molecule": { + "moleculeName": "label", + "text": "Vishal Verizon\n555.555.5557\nGo Unlimited", + "attributes": [{ + "location": 0, + "length": 14, + "type": "font", + "style": "B1" + }] + } + },{ + "horizontalAlignment": "trailing", + "molecule": { + "moleculeName": "label", + "text": "0 GB" + } + }] + }, + "actionMap":{ + "actionType": "openPage", + "pageType": "usageDetails", + "extraParameters": { + "mdn": "5555555557" + } + }}] +} \ No newline at end of file diff --git a/MVMCoreUI/Templates/MoleculeListCellProtocol.h b/MVMCoreUI/Templates/MoleculeListCellProtocol.h index 08ca2a37..7603c820 100644 --- a/MVMCoreUI/Templates/MoleculeListCellProtocol.h +++ b/MVMCoreUI/Templates/MoleculeListCellProtocol.h @@ -10,8 +10,6 @@ @protocol MoleculeListCellProtocol @optional -/// Can override the molecule name for the given molecule. Otherwise we assume value for key moleculeName. -+ (nullable NSString *)moleculeName:(nullable NSDictionary *)molecule delegateObject:(nullable MVMCoreUIDelegateObject *)delegateObject; /// Can set the separator according to what the moleculeList commands. - (void)setSeparatorWithJSON:(nullable NSDictionary *)json delegateObject:(nullable MVMCoreUIDelegateObject *)delegateObject additionalData:(nullable NSDictionary *)additionalData indexPath:(nonnull NSIndexPath *)indexPath; diff --git a/MVMCoreUI/Templates/MoleculeListTemplate.swift b/MVMCoreUI/Templates/MoleculeListTemplate.swift index 9f8b7707..ea3f94b4 100644 --- a/MVMCoreUI/Templates/MoleculeListTemplate.swift +++ b/MVMCoreUI/Templates/MoleculeListTemplate.swift @@ -36,7 +36,7 @@ open class MoleculeListTemplate: ThreeLayerTableViewController { override open func viewForBottom() -> UIView { guard let moleculeJSON = loadObject?.pageJSON?.optionalDictionaryForKey("footer"), let molecule = MVMCoreUIMoleculeMappingObject.shared()?.createMolecule(forJSON: moleculeJSON, delegateObject: delegateObject() as? MVMCoreUIDelegateObject, constrainIfNeeded: true) else { - return viewForBottom() + return super.viewForBottom() } return molecule } @@ -117,7 +117,7 @@ open class MoleculeListTemplate: ThreeLayerTableViewController { guard let map = molecule.molecule, let moleculeName = map.optionalStringForKey(KeyMoleculeName), let moleculeClass = MVMCoreUIMoleculeMappingObject.shared()?.moleculeMapping[moleculeName] as? AnyClass else { return nil } - if let moleculeClass = moleculeClass as? MoleculeListCellProtocol.Type, let moleculeNameFunc = moleculeClass.moleculeName { + if let moleculeClass = moleculeClass as? MVMCoreUIMoleculeViewProtocol.Type, let moleculeNameFunc = moleculeClass.name { return (moleculeNameFunc(map, delegateObject() as? MVMCoreUIDelegateObject), moleculeClass) } else { return (moleculeName, moleculeClass) diff --git a/MVMCoreUI/Utility/MVMCoreUICommonViewsUtility.m b/MVMCoreUI/Utility/MVMCoreUICommonViewsUtility.m index affbc470..487a7b51 100644 --- a/MVMCoreUI/Utility/MVMCoreUICommonViewsUtility.m +++ b/MVMCoreUI/Utility/MVMCoreUICommonViewsUtility.m @@ -197,7 +197,13 @@ static const CGFloat VertialShadowOffset = 6; [view.rightAnchor constraintEqualToAnchor:button.rightAnchor constant:PaddingTwo].active = YES; [view.centerYAnchor constraintEqualToAnchor:button.centerYAnchor].active = YES; } else { - [NSLayoutConstraint constraintPinSubview:button pinTop:YES topConstant:PaddingOne pinBottom:NO bottomConstant:0 pinLeft:NO leftConstant:0 pinRight:YES rightConstant:PaddingTwo]; + if (@available(iOS 11.0, *)) { + [button.topAnchor constraintEqualToAnchor:view.safeAreaLayoutGuide.topAnchor constant:PaddingOne].active = YES; + [view.safeAreaLayoutGuide.trailingAnchor constraintEqualToAnchor:button.trailingAnchor constant:PaddingTwo].active = YES; + } else { + [NSLayoutConstraint constraintPinSubview:button pinTop:YES topConstant:PaddingOne pinBottom:NO bottomConstant:0 pinLeft:NO leftConstant:0 pinRight:YES rightConstant:PaddingTwo]; + } + } } return button;