// // 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? init(with view: UIView) { self.view = view } init(with view: UIView, json: [AnyHashable: Any]) { self.view = view 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 spacingBlock: ((Any) -> UIEdgeInsets)? var useMargins: Bool = false var contentView: UIView = MVMCoreUICommonViewsUtility.commonView() var items: [StackItem] = [] private var spacingConstraints: [NSLayoutConstraint] = [] /// For setting the direction of the stack var axis: NSLayoutConstraint.Axis = .vertical { didSet { if axis != oldValue { restack() } } } var spacing: CGFloat = 16 { didSet { if spacing != oldValue { MVMCoreDispatchUtility.performBlock(onMainThread: { // loop space bettwen constraints and update. skip custom ones... }) } } } 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) -> NSLayoutConstraint { let constraint = NSLayoutConstraint(item: view, attribute: attribute, relatedBy: relation, toItem: toView, attribute: attribute, multiplier: 1.0, constant: constant) constraint.priority = priority constraint.isActive = true return constraint } // 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 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") } // MARK: - MFViewProtocol public override func setupView() { super.setupView() guard contentView.superview == nil else { return } translatesAutoresizingMaskIntoConstraints = false backgroundColor = .clear 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) } } } // MARK: - MVMCoreUIMoleculeViewProtocol public override func reset() { 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]?) { super.setWithJSON(json, delegateObject: delegateObject, additionalData: additionalData) clear() guard let molecules = json?.arrayForKey(KeyMolecules) as? [[String: Any]] else { return } // Sets the stack attributes setAxisWithJSON(json) spacing = json?.optionalCGFloatForKey("spacing") ?? 16 // 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 { alignHorizontal(ViewConstrainingView.getAlignmentFor(json?.optionalStringForKey("alignment"), defaultAlignment: .fill)) alignVertical(.leading) } // Create the molecules and set the json. for (index, map) in molecules.enumerated() { if let moleculeJSON = map.optionalDictionaryForKey(KeyMolecule), let molecule = MVMCoreUIMoleculeMappingObject.shared()?.createMolecule(forJSON: moleculeJSON, delegateObject: delegateObject, constrainIfNeeded: true) { addStackItem(StackItem(with: molecule, json: map), lastItem: index == molecules.count - 1) /*contentView.addSubview(molecule) molecule.translatesAutoresizingMaskIntoConstraints = false let spacing = CGFloat(map["spacing"] as? Float ?? self.spacing) let percent = map["percent"] as? Int let verticalAlignment = ViewConstrainingView.getAlignmentFor(map.optionalStringForKey("verticalAlignment"), defaultAlignment: (percent == nil && axis == .vertical ? UIStackView.Alignment.fill : UIStackView.Alignment.leading)) let horizontalAlignment = ViewConstrainingView.getAlignmentFor(map.optionalStringForKey("horizontalAlignment"), defaultAlignment: (axis == .vertical || percent == nil ? UIStackView.Alignment.fill : UIStackView.Alignment.leading)) if let molecule = molecule as? MVMCoreUIViewConstrainingProtocol { molecule.alignHorizontal?(horizontalAlignment) molecule.alignVertical?(verticalAlignment) } if axis == .vertical { if index == 0 { pinView(molecule, toView: previousObject, attribute: .top, relation: .equal, priority: .required, constant: spacing) } else { NSLayoutConstraint(pinFirstView: previousObject, toSecondView: molecule, withConstant: spacing, directionVertical: true)?.isActive = true } pinView(molecule, toView: contentView, attribute: .leading, relation: .equal, priority: .required, constant: 0) pinView(contentView, toView: molecule, attribute: .trailing, relation: .equal, priority: .required, constant: 0) if let percent = percent { molecule.heightAnchor.constraint(equalTo: contentView.heightAnchor, multiplier: CGFloat(percent)/100.0).isActive = true } } else { if index == 0 { pinView(molecule, toView: previousObject, attribute: .leading, relation: .equal, priority: .required, constant: spacing) } else { NSLayoutConstraint(pinFirstView: previousObject, toSecondView: molecule, withConstant: spacing, directionVertical: false)?.isActive = true } pinView(molecule, toView: contentView, attribute: .top, relation: .equal, priority: .required, constant: 0) pinView(contentView, toView: molecule, attribute: .bottom, relation: .equal, priority: .required, constant: 0) if let percent = percent { molecule.widthAnchor.constraint(equalTo: contentView.widthAnchor, multiplier: CGFloat(percent)/100.0).isActive = true } } previousObject = molecule } } if axis == .vertical { pinView(contentView, toView: previousObject, attribute: .bottom, relation: .equal, priority: .required, constant: 0) } else { pinView(contentView, toView: previousObject, attribute: .right, relation: .equal, priority: .required, constant: 0) }*/ } } } public override static func name(forReuse molecule: [AnyHashable : Any]?, delegateObject: MVMCoreUIDelegateObject?) -> String? { 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: - Convenience Functions func clear() { MVMCoreUIStackableViewController.remove(contentView.subviews) spacingConstraints = [] } func restack() { clear() let stackItems = items items.removeAll() for (index, item) in stackItems.enumerated() { addStackItem(item, lastItem: index == stackItems.count - 1) } } /// 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 { spacingConstraints.append(pinView(view, toView: contentView, attribute: .top, relation: .equal, priority: .required, constant: spacing)) } else if let previousView = items.last?.view { spacingConstraints.append(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. spacingConstraints.append(pinView(view, toView: contentView, attribute: .leading, relation: .equal, priority: .required, constant: stackItem.spacing ?? 0)) } else if let previousView = items.last?.view { spacingConstraints.append(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) } }