mvm_core_ui/MVMCoreUI/Organisms/MoleculeStackView.swift

269 lines
12 KiB
Swift

//
// 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.Type,
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<MVMCoreErrorObject?>?) -> [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.percent == nil && stackModel.axis == .vertical ? .fill : (stackModel.axis == .vertical ? .leading : .center))
let horizontalAlignment = model.horizontalAlignment ?? (stackItem.view as? MVMCoreUIViewConstrainingProtocol)?.horizontalAlignment?() ?? (stackModel.axis == .vertical || model.percent == 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.percent {
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.percent {
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)
}
}