diff --git a/MVMCoreUI.xcodeproj/project.pbxproj b/MVMCoreUI.xcodeproj/project.pbxproj index c1f4a798..17fc19f7 100644 --- a/MVMCoreUI.xcodeproj/project.pbxproj +++ b/MVMCoreUI.xcodeproj/project.pbxproj @@ -42,6 +42,7 @@ D260D7B622D68514007E7233 /* MVMCoreUIPagingProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = D260D7B522D68509007E7233 /* MVMCoreUIPagingProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; D274CA332236A78900B01B62 /* StandardFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D274CA322236A78900B01B62 /* StandardFooterView.swift */; }; D27CD40E2322EEAF00C1DC07 /* TabsTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D27CD40D2322EEAF00C1DC07 /* TabsTableViewCell.swift */; }; + D27CD4102339057800C1DC07 /* EyebrowHeadlineBodyLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = D27CD40F2339057800C1DC07 /* EyebrowHeadlineBodyLink.swift */; }; D282AAB4223FDDAE00C46919 /* MFLoadImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D282AAB3223FDDAE00C46919 /* MFLoadImageView.swift */; }; D282AABA224131D100C46919 /* MFTransparentGIFView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D282AAB9224131D100C46919 /* MFTransparentGIFView.swift */; }; D282AACB2243C61700C46919 /* ButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D282AACA2243C61700C46919 /* ButtonView.swift */; }; @@ -226,6 +227,7 @@ D260D7B522D68509007E7233 /* MVMCoreUIPagingProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MVMCoreUIPagingProtocol.h; sourceTree = ""; }; D274CA322236A78900B01B62 /* StandardFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StandardFooterView.swift; sourceTree = ""; }; D27CD40D2322EEAF00C1DC07 /* TabsTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabsTableViewCell.swift; sourceTree = ""; }; + D27CD40F2339057800C1DC07 /* EyebrowHeadlineBodyLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EyebrowHeadlineBodyLink.swift; sourceTree = ""; }; D282AAB3223FDDAE00C46919 /* MFLoadImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MFLoadImageView.swift; sourceTree = ""; }; D282AAB9224131D100C46919 /* MFTransparentGIFView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MFTransparentGIFView.swift; sourceTree = ""; }; D282AACA2243C61700C46919 /* ButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonView.swift; sourceTree = ""; }; @@ -418,6 +420,7 @@ children = ( D2A638FC22CA98280052ED1F /* HeadlineBody.swift */, D22479952316AF6D003FCCF9 /* HeadlineBodyTextButton.swift */, + D27CD40F2339057800C1DC07 /* EyebrowHeadlineBodyLink.swift */, ); path = VerticalCombinationViews; sourceTree = ""; @@ -1067,6 +1070,7 @@ D22D1F47220496A30077CEC0 /* MVMCoreUISwitch.m in Sources */, D29DF28C21E7AC2B003B2FB9 /* ViewConstrainingView.m in Sources */, D29DF17B21E69E1F003B2FB9 /* PrimaryButton.m in Sources */, + D27CD4102339057800C1DC07 /* EyebrowHeadlineBodyLink.swift in Sources */, D29DF11D21E684A9003B2FB9 /* MVMCoreUISplitViewController.m in Sources */, 0198F79F225679880066C936 /* FormValidationProtocol.swift in Sources */, D29DF29821E7ADB8003B2FB9 /* MFScrollingViewController.m in Sources */, diff --git a/MVMCoreUI/Molecules/VerticalCombinationViews/EyebrowHeadlineBodyLink.swift b/MVMCoreUI/Molecules/VerticalCombinationViews/EyebrowHeadlineBodyLink.swift new file mode 100644 index 00000000..306cf690 --- /dev/null +++ b/MVMCoreUI/Molecules/VerticalCombinationViews/EyebrowHeadlineBodyLink.swift @@ -0,0 +1,70 @@ +// +// EyebrowHeadlineBodyLink.swift +// MVMCoreUI +// +// Created by Scott Pfeil on 9/23/19. +// Copyright © 2019 Verizon Wireless. All rights reserved. +// + +import UIKit + +@objcMembers open class EyebrowHeadlineBodyLink: ViewConstrainingView { + let stack = MoleculeStackView(frame: .zero) + let eyebrow = Label.commonLabelB3(true) + let headline = Label.commonLabelB1(true) + let body = Label.commonLabelB2(true) + let link = MFTextButton(nil, constrainHeight: false, forWidth: MVMCoreUIUtility.getWidth()) + + // MARK: - MFViewProtocol + open override func setupView() { + super.setupView() + guard stack.superview == nil else { + return + } + stack.spacing = 0 + stack.updateViewHorizontalDefaults = false + addSubview(stack) + pinView(toSuperView: stack) + stack.addStackItem(StackItem(with: eyebrow), lastItem: false) + stack.addStackItem(StackItem(with: headline), lastItem: false) + stack.addStackItem(StackItem(with: body), lastItem: false) + + // To visually take into account the extra padding in the intrinsic content of a button. + let stackItem = StackItem(with: link) + stackItem.spacing = -6 + stack.addStackItem(stackItem, lastItem: true) + } + + open override func updateView(_ size: CGFloat) { + super.updateView(size) + stack.updateView(size) + } + + // MARK: - MVMCoreUIMoleculeViewProtocol + open override func setWithJSON(_ json: [AnyHashable : Any]?, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable : Any]?) { + super.setWithJSON(json, delegateObject: delegateObject, additionalData: additionalData) + eyebrow.setWithJSON(json?.optionalDictionaryForKey("eyebrow"), delegateObject: delegateObject, additionalData: additionalData) + stack.items[0].gone = !eyebrow.hasText + headline.setWithJSON(json?.optionalDictionaryForKey("headline"), delegateObject: delegateObject, additionalData: additionalData) + stack.items[1].gone = !headline.hasText + body.setWithJSON(json?.optionalDictionaryForKey("body"), delegateObject: delegateObject, additionalData: additionalData) + stack.items[2].gone = !body.hasText + link.setWithJSON(json?.optionalDictionaryForKey("link"), delegateObject: delegateObject, additionalData: additionalData) + stack.items[3].gone = link.titleLabel?.text?.count ?? 0 == 0 + stack.restack() + } + + open override func reset() { + super.reset() + stack.reset() + stack.spacing = 0 + stack.updateViewHorizontalDefaults = false + eyebrow.styleB3(true) + headline.styleB1(true) + body.styleB2(true) + } + + public override static func estimatedHeight(forRow json: [AnyHashable : Any]?, delegateObject: MVMCoreUIDelegateObject?) -> CGFloat { + return 65 + } +} diff --git a/MVMCoreUI/Organisms/MoleculeStackView.swift b/MVMCoreUI/Organisms/MoleculeStackView.swift index b2f70e6d..14ad9e70 100644 --- a/MVMCoreUI/Organisms/MoleculeStackView.swift +++ b/MVMCoreUI/Organisms/MoleculeStackView.swift @@ -14,25 +14,27 @@ public class StackItem { var percentage: Int? var verticalAlignment: UIStackView.Alignment? var horizontalAlignment: UIStackView.Alignment? + var gone = false init(with view: UIView) { self.view = view } - init(with view: UIView, json: [AnyHashable: Any]) { + 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.stringOptionalWithChainOfKeysOrIndexes([KeyMolecule,"verticalAlignment"]) { + func update(with json: [AnyHashable: Any]?) { + gone = json?.boolForKey("gone") ?? (json == nil) + spacing = json?.optionalCGFloatForKey("spacing") + percentage = json?["percent"] as? Int + if let alignment = json?.stringOptionalWithChainOfKeysOrIndexes([KeyMolecule,"verticalAlignment"]) { verticalAlignment = ViewConstrainingView.getAlignmentFor(alignment, defaultAlignment: .fill) } else { verticalAlignment = nil } - if let alignment = json.stringOptionalWithChainOfKeysOrIndexes([KeyMolecule,"horizontalAlignment"]) { + if let alignment = json?.stringOptionalWithChainOfKeysOrIndexes([KeyMolecule,"horizontalAlignment"]) { horizontalAlignment = ViewConstrainingView.getAlignmentFor(alignment, defaultAlignment: .fill) } else { horizontalAlignment = nil @@ -84,11 +86,13 @@ public class MoleculeStackView: ViewConstrainingView { /// 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) + setWithStackItems(items) + } + + /// Removes all stack items views from the view. + func removeAllItemViews() { + for item in items { + item.view.removeFromSuperview() } } @@ -144,7 +148,7 @@ public class MoleculeStackView: ViewConstrainingView { 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) + removeAllItemViews() // If the items in the stack are the same, just update previous items instead of re-allocating. var items: [StackItem]? @@ -238,6 +242,10 @@ public class MoleculeStackView: ViewConstrainingView { /// Adds the stack item to the stack. func addStackItem(_ stackItem: StackItem, lastItem: Bool) { + guard !stackItem.gone else { + items.append(stackItem) + return + } let view = stackItem.view contentView.addSubview(view) view.translatesAutoresizingMaskIntoConstraints = false @@ -249,10 +257,13 @@ public class MoleculeStackView: ViewConstrainingView { view.alignHorizontal?(horizontalAlignment) view.alignVertical?(verticalAlignment) } + let first = items.first { !$0.gone } == nil if axis == .vertical { - if items.count == 0 { + if first { pinView(view, toView: contentView, attribute: .top, relation: .equal, priority: .required, constant: useStackSpacingBeforeFirstItem ? spacing : stackItem.spacing ?? 0) - } else if let previousView = items.last?.view { + } else if let previousView = items.last(where: { stackItem in + return !stackItem.gone + })?.view { _ = NSLayoutConstraint(pinFirstView: previousView, toSecondView: view, withConstant: spacing, directionVertical: true) } pinView(view, toView: contentView, attribute: .leading, relation: .equal, priority: .required, constant: 0) @@ -264,10 +275,12 @@ public class MoleculeStackView: ViewConstrainingView { pinView(contentView, toView: view, attribute: .bottom, relation: .equal, priority: .required, constant: 0) } } else { - if items.count == 0 { + if first { // First horizontal item has no spacing by default unless told otherwise. pinView(view, toView: contentView, attribute: .leading, relation: .equal, priority: .required, constant: useStackSpacingBeforeFirstItem ? spacing : stackItem.spacing ?? 0) - } else if let previousView = items.last?.view { + } else if let previousView = items.last(where: { stackItem in + return !stackItem.gone + })?.view { _ = NSLayoutConstraint(pinFirstView: previousView, toSecondView: view, withConstant: spacing, directionVertical: false) } pinView(view, toView: contentView, attribute: .top, relation: .equal, priority: .required, constant: 0) @@ -281,4 +294,20 @@ public class MoleculeStackView: ViewConstrainingView { } items.append(stackItem) } + + func setWithStackItems(_ items: [StackItem]) { + removeAllItemViews() + self.items.removeAll() + var previousPresentItem: StackItem? = nil + for item in items { + if !item.gone { + previousPresentItem = item + } + addStackItem(item, lastItem: false) + } + if let lastView = previousPresentItem?.view { + let attribute: NSLayoutConstraint.Attribute = axis == .vertical ? .bottom : .right + pinView(contentView, toView: lastView, attribute: attribute, relation: .equal, priority: .required, constant: 0) + } + } } diff --git a/MVMCoreUI/OtherHandlers/MVMCoreUIMoleculeMappingObject.m b/MVMCoreUI/OtherHandlers/MVMCoreUIMoleculeMappingObject.m index 86267332..945eb7bf 100644 --- a/MVMCoreUI/OtherHandlers/MVMCoreUIMoleculeMappingObject.m +++ b/MVMCoreUI/OtherHandlers/MVMCoreUIMoleculeMappingObject.m @@ -60,7 +60,8 @@ @"headlineBodySwitch": HeadlineBodySwitch.class, @"headlineBodyTextButton": HeadlineBodyTextButton.class, @"headlineBodyTextButtonSwitch": HeadlineBodyTextButtonSwitch.class, - @"tabsListItem": TabsTableViewCell.class + @"tabsListItem": TabsTableViewCell.class, + @"eyebrowHeadlineBodyLink": EyebrowHeadlineBodyLink.class } mutableCopy]; }); return mapping;