// // MoleculeStackView.swift // MVMCoreUI // // Created by Scott Pfeil on 2/11/19. // Copyright © 2019 Verizon Wireless. All rights reserved. // import UIKit public class StackItem { var view: UIView var spacing: CGFloat? 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, stackItemModel: MoleculeStackItemModel?) { self.view = view update(with: stackItemModel) } func update(with stackItemModel: MoleculeStackItemModel?) { gone = stackItemModel?.gone ?? false spacing = stackItemModel?.spacing percentage = stackItemModel?.percentage if let alignment = stackItemModel?.verticalAlignment { verticalAlignment = ViewConstrainingView.getAlignmentFor(alignment, defaultAlignment: .fill) } else { verticalAlignment = nil } if let alignment = stackItemModel?.horizontalAlignment { horizontalAlignment = ViewConstrainingView.getAlignmentFor(alignment, defaultAlignment: .fill) } else { horizontalAlignment = nil } } } public class MoleculeStackView: ViewConstrainingView, ModelMoleculeViewProtocol { var contentView: UIView = MVMCoreUICommonViewsUtility.commonView() var stackItems: [StackItem] = [] var useStackSpacingBeforeFirstItem = false var moleculeStackModel: MoleculeStackModel? private var moleculesShouldSetHorizontalMargins = false private var moleculesShouldSetVerticalMargins = false /// For setting the direction of the stack var axis: NSLayoutConstraint.Axis = .vertical { didSet { 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() { setWithStackItems(stackItems) } /// Removes all stack items views from the view. func removeAllItemViews() { for item in stackItems { item.view.removeFromSuperview() } } // MARK: - Inits public override init(frame: CGRect) { super.init(frame: frame) } public init(withJSON json: [AnyHashable: Any]?, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable : Any]?) { super.init(frame: CGRect.zero) setWithJSON(json, delegateObject: delegateObject, additionalData: additionalData) } public required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } // MARK: - MFViewProtocol public override func setupView() { super.setupView() guard contentView.superview == nil else { return } MVMCoreUIUtility.setMarginsFor(contentView, leading: 0, top: 0, trailing: 0, bottom: 0) updateViewHorizontalDefaults = true translatesAutoresizingMaskIntoConstraints = false backgroundColor = .clear addSubview(contentView) pinView(toSuperView: contentView) contentView.setContentHuggingPriority(.defaultHigh, for: .vertical) contentView.setContentHuggingPriority(.defaultHigh, for: .horizontal) } public override func updateView(_ size: CGFloat) { super.updateView(size) for item in stackItems { (item.view as? MVMCoreViewProtocol)?.updateView(size) } } // MARK: - MVMCoreUIMoleculeViewProtocol public override func reset() { super.reset() backgroundColor = .clear updateViewHorizontalDefaults = true for item in stackItems { if let view = item.view as? MVMCoreUIMoleculeViewProtocol { view.reset?() } } } //TODO: Model, Change to model public func setWithModel(_ model: MoleculeProtocol?, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [String : AnyHashable]?) { guard let model = model as? MoleculeStackModel else { return } let previousModel = self.moleculeStackModel self.moleculeStackModel = model #warning("This below call should be repaced with super.setWithModel once we get rid of ViewConstrainingView.") //TODO: This below call should be repaced with super.setWithModel once we get rid of ViewConstrainingView. setUpWithModel(model, delegateObject, additionalData) removeAllItemViews() // If the items in the stack are the same, just update previous items instead of re-allocating. var items: [StackItem]? if let previousModel = previousModel { let previoudReuseName = MoleculeStackView.name(forReuse: previousModel, delegateObject: delegateObject) let currentReuseName = MoleculeStackView.name(forReuse: model, delegateObject: delegateObject) if previoudReuseName == currentReuseName { items = self.stackItems } } self.stackItems = [] guard let molecules = model.molecules else { return } // Sets the stack attributes //setAxisWithJSON(json) spacing = CGFloat(model.spacing ?? 16) for (index, stackItemModel) in molecules.enumerated() { if let moleculeModel = stackItemModel.molecule { var view: UIView? if let item = items?[index] { item.update(with: stackItemModel) view = item.view (view as? ModelMoleculeViewProtocol)?.setWithModel(moleculeModel, delegateObject, nil) addStackItem(item, lastItem: index == molecules.count - 1) } else if let moleculeView = MVMCoreUIMoleculeMappingObject.shared()?.createMolecule(moleculeModel, delegateObject, true) { view = moleculeView addStackItem(StackItem(with: moleculeView, stackItemModel: stackItemModel), lastItem: index == molecules.count - 1) } (view as? MVMCoreUIViewConstrainingProtocol)?.shouldSetHorizontalMargins?(moleculesShouldSetHorizontalMargins) (view as? MVMCoreUIViewConstrainingProtocol)?.shouldSetVerticalMargins?(moleculesShouldSetVerticalMargins) } } } public override class func name(forReuse molecule: [AnyHashable : Any]?, delegateObject: MVMCoreUIDelegateObject?) -> String? { // This will aggregate names of molecules to make an id. guard let molecules = molecule?.optionalArrayForKey(KeyMolecules) else { return "stack<>" } var name = "stack<" for case let item as [AnyHashable: Any] in molecules { if let molecule = item.optionalDictionaryForKey(KeyMolecule), let moleculeName = MVMCoreUIMoleculeMappingObject.shared()?.getMoleculeClass(withJSON: molecule)?.name?(forReuse: molecule, delegateObject: delegateObject) ?? molecule.optionalStringForKey(KeyMoleculeName) { name.append(moleculeName + ",") } } name.append(">") return name } public override class func estimatedHeight(forRow json: [AnyHashable : Any]?, delegateObject: MVMCoreUIDelegateObject?) -> CGFloat { guard let items = json?.optionalArrayForKey(KeyMolecules) else { return 0 } let horizontal = json?.optionalStringForKey("axis") == "horizontal" var estimatedHeight: CGFloat = 0 for case let item as [AnyHashable: AnyHashable] in items { if let molecule = item.optionalDictionaryForKey(KeyMolecule) { let height = MVMCoreUIMoleculeMappingObject.shared()?.getMoleculeClass(withJSON: molecule)?.estimatedHeight?(forRow: molecule, delegateObject: delegateObject) if !horizontal { // Vertical stack aggregates the items let spacing = item.optionalCGFloatForKey("spacing") ?? (estimatedHeight != 0 ? (json?.optionalCGFloatForKey("spacing") ?? 16) : 0) estimatedHeight += ((height ?? 0) + spacing) } else if let height = height { // Horizontal stack takes the tallest item. estimatedHeight = max(estimatedHeight, height) } } } return estimatedHeight } public override class func requiredModules(_ json: [AnyHashable : Any]?, delegateObject: MVMCoreUIDelegateObject?, error: AutoreleasingUnsafeMutablePointer?) -> [String]? { guard let items = json?.optionalArrayForKey(KeyMolecules) else { return nil } var modules: [String] = [] for case let item as [AnyHashable: AnyHashable] in items { if let molecule = item.optionalDictionaryForKey(KeyMolecule), let modulesForMolecule = MVMCoreUIMoleculeMappingObject.shared()?.getMoleculeClass(withJSON: molecule)?.requiredModules?(molecule, delegateObject: delegateObject, error: error) { modules += modulesForMolecule } } return modules.count > 0 ? modules : nil } // 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) { guard !stackItem.gone else { stackItems.append(stackItem) return } 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) } let first = stackItems.first { !$0.gone } == nil if axis == .vertical { if first { pinView(view, toView: contentView, attribute: .top, relation: .equal, priority: .required, constant: useStackSpacingBeforeFirstItem ? spacing : stackItem.spacing ?? 0) } else if let previousView = stackItems.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) 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 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 = stackItems.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) 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) } stackItems.append(stackItem) } func setWithStackItems(_ items: [StackItem]) { removeAllItemViews() self.stackItems.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) } } }