mvm_core_ui/MVMCoreUI/Organisms/Stack.swift
2020-02-28 14:08:38 -05:00

333 lines
15 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 & MoleculeModelProtocol) {
//--------------------------------------------------
// MARK: - Properties
//--------------------------------------------------
open var contentView: UIView = MVMCoreUICommonViewsUtility.commonView()
open var stackModel: T? {
get { return model as? T }
}
open var stackItems: [UIView] = []
//--------------------------------------------------
// MARK: - Helpers
//--------------------------------------------------
open 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.
open 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
})
// Adds the views
let totalSpace = getTotalSpace()
for (index, view) in stackItems.enumerated() {
addView(view, stackModel.molecules[index], totalSpacing: totalSpace, lastItem: lastItemIndex == index)
}
}
/// Removes all stack items views from the view.
open func removeAllItemViews() {
for item in stackItems {
item.removeFromSuperview()
}
}
//--------------------------------------------------
// MARK: - Initializers
//--------------------------------------------------
public override init(frame: CGRect) {
super.init(frame: frame)
}
/// The main initializer for model driven
public init(with model: T, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) {
super.init(frame: .zero)
setOptional(with: model, delegateObject, additionalData)
}
/// The main initializer for hardcode driven
public init(with model: T, stackItems: [UIView]) {
super.init(frame: CGRect.zero)
self.model = model
self.stackItems = stackItems
}
public required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
/// Returns a Stack created with a StackModel and StackItems containing the passed in views.
public static func createStack(with views: [UIView], axis: NSLayoutConstraint.Axis? = nil, spacing: CGFloat? = nil) -> Stack<StackModel> {
var items: [StackItem] = []
var models: [StackItemModel] = []
for view in views {
items.append(StackItem(andContain: view))
models.append(StackItemModel())
}
let model = StackModel(molecules: models, axis: axis, spacing: spacing)
return Stack<StackModel>(with: model, stackItems: items)
}
/// Returns a Stack created with a StackModel containing the passed in views and using the passed in stackitems.
public static func createStack(with viewModels:[(view: UIView, model: StackItemModel)], axis: NSLayoutConstraint.Axis? = nil, spacing: CGFloat? = nil) -> Stack<StackModel> {
var stackItems: [StackItem] = []
var models: [StackItemModel] = []
for item in viewModels {
stackItems.append(StackItem(andContain: item.view))
models.append(item.model)
}
let model = StackModel(molecules: models, axis: axis, spacing: spacing)
return Stack<StackModel>(with: model, stackItems: stackItems)
}
//--------------------------------------------------
// MARK: - MFViewProtocol
//--------------------------------------------------
open 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)
}
open override func updateView(_ size: CGFloat) {
super.updateView(size)
for item in stackItems {
(item as? MVMCoreViewProtocol)?.updateView(size)
}
}
//--------------------------------------------------
// MARK: - MVMCoreUIMoleculeViewProtocol
//--------------------------------------------------
open override func reset() {
super.reset()
backgroundColor = .clear
for item in stackItems {
(item as? MoleculeViewProtocol)?.reset?()
}
}
open override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
let previousModel = self.model
super.set(with: model, delegateObject, additionalData)
removeAllItemViews()
// If the items in the stack are different, clear them, create new ones.
if (previousModel == nil) || Self.nameForReuse(with: previousModel!, delegateObject) != Self.nameForReuse(with: model, delegateObject) {
stackItems = []
createStackItemsFromModel(model, delegateObject, additionalData)
} else {
setStackItemsFromModel(model, delegateObject, additionalData)
}
restack()
}
open override class func nameForReuse(with 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(with: item, delegateObject) {
name.append(nameForReuse + ",")
} else {
name.append(moleculeName + ",")
}
}
}
name.append(">")
return name
}
// Need to update to take into account first spacing flag
open override class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? {
guard let model = model 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(with: item, 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
}
open override class func requiredModules(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, error: AutoreleasingUnsafeMutablePointer<MVMCoreErrorObject?>?) -> [String]? {
guard let model = model 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(with: item, 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
open func createStackItemsFromModel(_ model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { }
/// Can be subclassed to set stack items with model when we already have views
open 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)?.set(with: element, delegateObject, additionalData)
}
}
//--------------------------------------------------
// MARK: - Adding to stack
//--------------------------------------------------
/// Sets the stack with StackItems containing the passed in views and creates a StackModel with StackItems.
open func setAndCreateModel(with views: [UIView]) {
var stackItems: [StackItem] = []
var models: [StackItemModel] = []
for view in views {
stackItems.append(StackItem(andContain: view))
models.append(StackItemModel())
}
self.stackItems = stackItems
model = StackModel(molecules: models)
}
/// Sets the stack with StackItems containing the passed in views and sets the StackModel with models.
open func set(with viewModels:[(view: UIView, model: T.AnyStackItemModel)]) {
guard var stackModel = self.stackModel else { return }
var stackItems: [StackItem] = []
var models: [T.AnyStackItemModel] = []
for item in viewModels {
stackItems.append(StackItem(andContain: item.view))
models.append(item.model)
}
stackModel.molecules = models
self.stackItems = stackItems
}
/// Gets the percent modifier. This value is used to help properly calculate percent for stack items when spacing is involved.
private func getTotalSpace() -> CGFloat {
guard let stackModel = stackModel else { return 0.0 }
var totalSpace: CGFloat = 0.0
var firstMoleculeFound = false
for stackItemModel in stackModel.molecules {
guard !stackItemModel.gone else { continue }
let spacing = stackItemModel.spacing ?? stackModel.spacing
if firstMoleculeFound {
totalSpace += spacing
} else {
firstMoleculeFound = true
totalSpace += (stackModel.useStackSpacingBeforeFirstItem ? spacing : stackItemModel.spacing ?? 0)
}
}
return totalSpace
}
/// Adds the stack item view
private func addView(_ view: UIView,_ model: StackItemModelProtocol, totalSpacing: CGFloat, 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 item.superview != nil
}) {
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 {
let multiplier = CGFloat(percent) / 100.0
let constant = multiplier * totalSpacing
view.heightAnchor.constraint(equalTo: contentView.heightAnchor, multiplier: multiplier, constant: -constant).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 item.superview != nil
}) {
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 {
let multiplier = CGFloat(percent) / 100.0
let constant = multiplier * totalSpacing
view.widthAnchor.constraint(equalTo: contentView.widthAnchor, multiplier: multiplier, constant: -constant).isActive = true
}
if lastItem {
pinView(contentView, toView: view, attribute: .right, relation: .equal, priority: .required, constant: 0)
}
}
stackItems.append(view)
}
}