// // MoleculeStackView.swift // MVMCoreUI // // Created by Scott Pfeil on 2/11/19. // Copyright © 2019 Verizon Wireless. All rights reserved. // import UIKit open class MoleculeStackView: Container { var contentView: UIView = MVMCoreUICommonViewsUtility.commonView() var useStackSpacingBeforeFirstItem = false var stackModel: MoleculeStackModel? { get { return model as? MoleculeStackModel } } var stackItems: [StackItem] = [] var moleculesShouldSetHorizontalMargins = false var moleculesShouldSetVerticalMargins = false // MARK: - Helpers 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() { removeAllItemViews() let stackItems = self.stackItems self.stackItems = [] let lastItem = stackItems.last(where: { (item) -> Bool in return !item.stackItemModel!.gone }) for item in stackItems { addStackItem(item, lastItem: item === lastItem) } } /// Removes all stack items views from the view. func removeAllItemViews() { for item in stackItems { item.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) translatesAutoresizingMaskIntoConstraints = false backgroundColor = .clear addSubview(contentView) containerHelper.constrainView(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.updateView(size) } } // MARK: - MVMCoreUIMoleculeViewProtocol public override func reset() { super.reset() backgroundColor = .clear for item in stackItems { item.reset() } } public override func setWithModel(_ model: MoleculeProtocol?, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable : Any]?) { let previousModel = stackModel super.setWithModel(model, delegateObject, additionalData) removeAllItemViews() // If the items in the stack are different, clear them, create new ones. if (previousModel == nil) || nameForReuse(previousModel, delegateObject) != nameForReuse(model, delegateObject) { stackItems = [] createStackItemsFromModel(with: delegateObject) } else if let models = stackModel?.molecules { for (index, element) in models.enumerated() { stackItems[index].setWithModel(element, delegateObject, additionalData) } } restack() stackModel?.useHorizontalMargins = moleculesShouldSetHorizontalMargins stackModel?.useVerticalMargins = moleculesShouldSetVerticalMargins } public override func nameForReuse(_ model: MoleculeProtocol?, _ delegateObject: MVMCoreUIDelegateObject?) -> String? { // This will aggregate names of molecules to make an id. guard let model = model as? MoleculeStackModel else { return "stack<>" } var name = "stack<" for case let item in model.molecules { if let moleculeName = item.molecule.moleculeName { if let moleculeClass = MVMCoreUIMoleculeMappingObject.shared()?.moleculeMapping[moleculeName] as? ModelMoleculeViewProtocol, let nameForReuse = moleculeClass.nameForReuse(item.molecule, delegateObject) { name.append(nameForReuse + ",") } else { name.append(moleculeName + ",") } } } name.append(">") return name } open override func setWithJSON(_ json: [AnyHashable: Any]?, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) { if model == nil { let data = try! JSONSerialization.data(withJSONObject: json!) let decoder = JSONDecoder() let model = try! decoder.decode(MoleculeStackModel.self, from: data) setWithModel(model, delegateObject, additionalData) } else { setWithModel(model, delegateObject, additionalData) } } public 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 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 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 /// Creates all of the stackItems for the stackItemModels func createStackItemsFromModel(with delegate: MVMCoreUIDelegateObject?) { guard let stackItemModels = stackModel?.molecules else { return } for model in stackItemModels { if let stackItem = MVMCoreUIMoleculeMappingObject.shared()?.createMolecule(model, delegate) as? StackItem { stackItems.append(stackItem) } } } /// Adds the view to the stack. func addView(_ view: View, lastItem: Bool) { guard let model = view.model else { return } let stackItem = StackItem(andContain: view) stackItem.model = StackItemModel(with: model) addStackItem(stackItem, lastItem: lastItem) } /// Adds the stack item view private func addStackItem(_ stackItem: StackItem, lastItem: Bool) { let stackModel = self.stackModel! let model = stackItem.stackItemModel! guard !model.gone else { // Gone views do not show return } contentView.addSubview(stackItem) stackItem.translatesAutoresizingMaskIntoConstraints = false let spacing = model.spacing ?? stackModel.spacing let verticalAlignment = model.verticalAlignment ?? (stackItem.view as? MVMCoreUIViewConstrainingProtocol)?.verticalAlignment?() ?? (model.percentage == nil && stackModel.axis == .vertical ? .fill : (stackModel.axis == .vertical ? .leading : .center)) let horizontalAlignment = model.horizontalAlignment ?? (stackItem.view as? MVMCoreUIViewConstrainingProtocol)?.horizontalAlignment?() ?? (stackModel.axis == .vertical || model.percentage == nil ? .fill : .leading) stackItem.containerHelper.alignHorizontal(horizontalAlignment) stackItem.containerHelper.alignVertical(verticalAlignment) let first = stackItems.first { !($0.stackItemModel?.gone ?? false) } == nil if stackModel.axis == .vertical { if first { pinView(stackItem, toView: contentView, attribute: .top, relation: .equal, priority: .required, constant: useStackSpacingBeforeFirstItem ? spacing : model.spacing ?? 0) } else if let previousView = stackItems.last(where: { item in return !item.stackItemModel!.gone }) { stackItem.topAnchor.constraint(equalTo: previousView.bottomAnchor, constant: spacing).isActive = true } pinView(stackItem, toView: contentView, attribute: .leading, relation: .equal, priority: .required, constant: 0) pinView(contentView, toView: stackItem, attribute: .trailing, relation: .equal, priority: .required, constant: 0) if let percent = model.percentage { stackItem.heightAnchor.constraint(equalTo: contentView.heightAnchor, multiplier: CGFloat(percent)/100.0).isActive = true } if lastItem { pinView(contentView, toView: stackItem, attribute: .bottom, relation: .equal, priority: .required, constant: 0) } } else { if first { // First horizontal item has no spacing by default unless told otherwise. pinView(stackItem, toView: contentView, attribute: .leading, relation: .equal, priority: .required, constant: useStackSpacingBeforeFirstItem ? spacing : model.spacing ?? 0) } else if let previousView = stackItems.last(where: { item in return !item.stackItemModel!.gone }) { stackItem.leftAnchor.constraint(equalTo: previousView.rightAnchor, constant: spacing).isActive = true } pinView(stackItem, toView: contentView, attribute: .top, relation: .equal, priority: .required, constant: 0) pinView(contentView, toView: stackItem, attribute: .bottom, relation: .equal, priority: .required, constant: 0) if let percent = model.percentage { stackItem.widthAnchor.constraint(equalTo: contentView.widthAnchor, multiplier: CGFloat(percent)/100.0).isActive = true } if lastItem { pinView(contentView, toView: stackItem, attribute: .right, relation: .equal, priority: .required, constant: 0) } } stackItems.append(stackItem) } }