237 lines
11 KiB
Swift
237 lines
11 KiB
Swift
//
|
|
// Stack.swift
|
|
// MVMCoreUI
|
|
//
|
|
// Created by Scott Pfeil on 1/16/20.
|
|
// Copyright © 2020 Verizon Wireless. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
open class Stack<T>: Container where T: StackModelProtocol {
|
|
var contentView: UIView = MVMCoreUICommonViewsUtility.commonView()
|
|
var stackModel: T? {
|
|
get { return model as? T }
|
|
}
|
|
var stackItems: [UIView] = []
|
|
|
|
// 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()
|
|
guard let stackModel = stackModel else { return }
|
|
let stackItems = self.stackItems
|
|
self.stackItems = []
|
|
let lastItemIndex = stackModel.molecules.lastIndex(where: { (item) -> Bool in
|
|
return !item.gone
|
|
})
|
|
for (index, view) in stackItems.enumerated() {
|
|
addView(view, stackModel.molecules[index], lastItem: lastItemIndex == index)
|
|
}
|
|
}
|
|
|
|
/// 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 as? MVMCoreViewProtocol)?.updateView(size)
|
|
}
|
|
}
|
|
|
|
// MARK: - MVMCoreUIMoleculeViewProtocol
|
|
public override func reset() {
|
|
super.reset()
|
|
backgroundColor = .clear
|
|
for item in stackItems {
|
|
(item as? MoleculeViewProtocol)?.reset?()
|
|
}
|
|
}
|
|
|
|
public override func setWithModel(_ model: MoleculeModelProtocol?, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable : Any]?) {
|
|
let previousModel = self.model
|
|
super.setWithModel(model, delegateObject, additionalData)
|
|
removeAllItemViews()
|
|
|
|
// If the items in the stack are different, clear them, create new ones.
|
|
if (previousModel == nil) || Self.nameForReuse(previousModel, delegateObject) != Self.nameForReuse(model, delegateObject) {
|
|
stackItems = []
|
|
createStackItemsFromModel(model, delegateObject, additionalData)
|
|
} else {
|
|
setStackItemsFromModel(model, delegateObject, additionalData)
|
|
}
|
|
|
|
restack()
|
|
}
|
|
|
|
public override class func nameForReuse(_ model: MoleculeModelProtocol?, _ delegateObject: MVMCoreUIDelegateObject?) -> String? {
|
|
// This will aggregate names of molecules to make an id.
|
|
guard let model = model as? T else {
|
|
return "stack<>"
|
|
}
|
|
var name = "stack<"
|
|
for case let item in model.molecules {
|
|
if let moleculeName = item.moleculeName {
|
|
if let moleculeClass = MVMCoreUIMoleculeMappingObject.shared()?.moleculeMapping[moleculeName] as? ModelMoleculeViewProtocol.Type,
|
|
let nameForReuse = moleculeClass.nameForReuse(item, delegateObject) {
|
|
name.append(nameForReuse + ",")
|
|
} else {
|
|
name.append(moleculeName + ",")
|
|
}
|
|
}
|
|
}
|
|
name.append(">")
|
|
return name
|
|
}
|
|
|
|
// Need to update to take into account first spacing flag
|
|
public override class func estimatedHeight(forRow molecule: MoleculeModelProtocol?, delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? {
|
|
guard let model = molecule as? T else { return 0 }
|
|
let horizontal = model.axis == .horizontal
|
|
var estimatedHeight: CGFloat = 0
|
|
for case let item in model.molecules {
|
|
if item.gone { continue }
|
|
let height = (MVMCoreUIMoleculeMappingObject.shared()?.getMoleculeClass(item) as? ModelMoleculeViewProtocol.Type)?.estimatedHeight(forRow: item, delegateObject: delegateObject) ?? 0
|
|
if !horizontal {
|
|
// Vertical stack aggregates the items
|
|
let spacing = item.spacing ?? model.spacing
|
|
estimatedHeight += (height + spacing)
|
|
} else {
|
|
// Horizontal stack takes the tallest item.
|
|
estimatedHeight = max(estimatedHeight, height)
|
|
}
|
|
}
|
|
return estimatedHeight
|
|
}
|
|
|
|
public override class func requiredModules(_ molecule: MoleculeModelProtocol?, delegateObject: MVMCoreUIDelegateObject?, error: AutoreleasingUnsafeMutablePointer<MVMCoreErrorObject?>?) -> [String]? {
|
|
guard let model = molecule as? T else { return nil }
|
|
var modules: [String] = []
|
|
for case let item in model.molecules {
|
|
if let modulesForMolecule = (MVMCoreUIMoleculeMappingObject.shared()?.getMoleculeClass(item) as? ModelMoleculeViewProtocol.Type)?.requiredModules(item, delegateObject: delegateObject, error: error) {
|
|
modules += modulesForMolecule
|
|
}
|
|
}
|
|
return modules.count > 0 ? modules : nil
|
|
}
|
|
|
|
// MARK: - Subclassables
|
|
|
|
/// Can be subclassed to create views when we get stack item models and have no views yet
|
|
func createStackItemsFromModel(_ model: MoleculeModelProtocol?, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable : Any]?) {
|
|
}
|
|
|
|
/// Can be subclassed to set stack items with model when we already have views
|
|
func setStackItemsFromModel(_ model: MoleculeModelProtocol?, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable : Any]?) {
|
|
guard let models = stackModel?.molecules else { return }
|
|
for (index, element) in models.enumerated() {
|
|
(stackItems[index] as? ModelMoleculeViewProtocol)?.setWithModel(element, delegateObject, additionalData)
|
|
}
|
|
}
|
|
|
|
// MARK: - Adding to stack
|
|
/// Convenience function, adds a view to a StackItem to the Stack
|
|
func addViewToItemToStack(_ view: UIView, lastItem: Bool) {
|
|
let stackItemModel = StackItemModel()
|
|
let stackItem = StackItem(andContain: view)
|
|
addView(stackItem, stackItemModel, lastItem: lastItem)
|
|
}
|
|
|
|
/// Adds the stack item view
|
|
func addView(_ view: UIView,_ model: StackItemModelProtocol, lastItem: Bool) {
|
|
guard let stackModel = self.stackModel else { return }
|
|
guard !model.gone else {
|
|
// Gone views do not show
|
|
stackItems.append(view)
|
|
return
|
|
}
|
|
contentView.addSubview(view)
|
|
view.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
let spacing = model.spacing ?? stackModel.spacing
|
|
if let container = view as? ContainerProtocol {
|
|
let verticalAlignment = (model as? ContainerModelProtocol)?.verticalAlignment ?? (container.view as? MVMCoreUIViewConstrainingProtocol)?.verticalAlignment?() ?? (model.percent == nil && stackModel.axis == .vertical ? .fill : (stackModel.axis == .vertical ? .leading : .center))
|
|
let horizontalAlignment = (model as? ContainerModelProtocol)?.horizontalAlignment ?? (container.view as? MVMCoreUIViewConstrainingProtocol)?.horizontalAlignment?() ?? (stackModel.axis == .vertical || model.percent == nil ? .fill : .leading)
|
|
container.alignHorizontal(horizontalAlignment)
|
|
container.alignVertical(verticalAlignment)
|
|
}
|
|
|
|
let first = contentView.subviews.count == 1
|
|
if stackModel.axis == .vertical {
|
|
if first {
|
|
pinView(view, toView: contentView, attribute: .top, relation: .equal, priority: .required, constant: stackModel.useStackSpacingBeforeFirstItem ? spacing : model.spacing ?? 0)
|
|
} else if let previousView = stackItems.last(where: { item in
|
|
return !model.gone
|
|
}) {
|
|
view.topAnchor.constraint(equalTo: previousView.bottomAnchor, constant: spacing).isActive = 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 = model.percent {
|
|
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: stackModel.useStackSpacingBeforeFirstItem ? spacing : model.spacing ?? 0)
|
|
} else if let previousView = stackItems.last(where: { item in
|
|
return !model.gone
|
|
}) {
|
|
view.leftAnchor.constraint(equalTo: previousView.rightAnchor, constant: spacing).isActive = true
|
|
}
|
|
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 = model.percent {
|
|
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(view)
|
|
}
|
|
}
|