mvm_core_ui/MVMCoreUI/Organisms/MoleculeStackView.swift
Pfeil, Scott Robert f610264327 alignment updates
2019-12-16 11:19:30 -05:00

285 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 items: [StackItemModel] = []
var useStackSpacingBeforeFirstItem = false
var moleculesShouldSetHorizontalMargins = false
var moleculesShouldSetVerticalMargins = false
/// For setting the direction of the stack
var axis: NSLayoutConstraint.Axis = .vertical {
didSet {
if axis != oldValue {
restack()
}
}
}
/// The spacing to use between each item in the stack.
var spacing: CGFloat = 16 {
didSet {
if spacing != oldValue {
restack()
}
}
}
// MARK: - Helpers
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) {
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() {
setWithStackItems(items)
}
/// Removes all stack items views from the view.
func removeAllItemViews() {
for item in items {
item.view.removeFromSuperview()
}
}
// MARK: - Inits
public override init() {
super.init()
}
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)
directionalLayoutMargins.leading = 0
directionalLayoutMargins.trailing = 0
for item in items {
item.view.updateView(size)
}
}
// MARK: - MVMCoreUIMoleculeViewProtocol
public override func reset() {
super.reset()
backgroundColor = .clear
for item in items {
item.view.reset()
}
}
open override func setWithJSON(_ json: [AnyHashable: Any]?, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) {
let previousJSON = self.json
super.setWithJSON(json, delegateObject: delegateObject, additionalData: additionalData)
removeAllItemViews()
// If the items in the stack are the same, just update previous items instead of re-allocating.
var items: [StackItemModel]?
if MoleculeStackView.name(forReuse: previousJSON, delegateObject: delegateObject) == MoleculeStackView.name(forReuse: json, delegateObject: delegateObject) {
items = self.items
}
self.items = []
guard let molecules = json?.arrayForKey(KeyMolecules) as? [[String: Any]] else {
return
}
// Sets the stack attributes
setAxisWithJSON(json)
spacing = json?.optionalCGFloatForKey("spacing") ?? 16
// Adds the molecules and sets the json.
for (index, map) in molecules.enumerated() {
var view: UIView?
var stackItemModel: StackItemModel
if let item = items?[index] {
stackItemModel = item
item.update(with: map)
view = item.view
(view as? MVMCoreUIMoleculeViewProtocol)?.setWithJSON(map, delegateObject: delegateObject, additionalData: nil)
addStackItem(item, lastItem: index == molecules.count - 1)
} else {
let stackItem = StackItem()
stackItem.setWithJSON(map, delegateObject: delegateObject, additionalData: additionalData)
view = stackItem
stackItemModel = StackItemModel(with: stackItem, json: map)
addStackItem(stackItemModel, lastItem: index == molecules.count - 1)
}
stackItemModel.useHorizontalMargins = moleculesShouldSetHorizontalMargins
stackItemModel.useVerticalMargins = moleculesShouldSetVerticalMargins
}
}
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
/// Adds the view to the stack.
func addView(_ view: UIView, lastItem: Bool) {
addStackItem(StackItemModel(with: StackItem(andContain: view)), lastItem: lastItem)
}
/// Adds the stack item to the stack.
func addStackItem(_ stackItem: StackItemModel, lastItem: Bool) {
guard !stackItem.gone else {
items.append(stackItem)
return
}
let view = stackItem.view
contentView.addSubview(view)
view.translatesAutoresizingMaskIntoConstraints = false
let spacing = stackItem.spacing ?? self.spacing
let verticalAlignment = stackItem.verticalAlignment ?? (stackItem.percentage == nil && axis == .vertical ? .fill : (axis == .vertical ? .leading : .center))
let horizontalAlignment = stackItem.horizontalAlignment ?? (view.view as? MVMCoreUIViewConstrainingProtocol)?.horizontalAlignment?() ?? (axis == .vertical || stackItem.percentage == nil ? .fill : .leading)
view.containerHelper.alignHorizontal(horizontalAlignment)
view.containerHelper.alignVertical(verticalAlignment)
let first = items.first { !$0.gone } == nil
if axis == .vertical {
if first {
pinView(view, toView: contentView, attribute: .top, relation: .equal, priority: .required, constant: useStackSpacingBeforeFirstItem ? spacing : stackItem.spacing ?? 0)
} else if let previousView = items.last(where: { stackItem in
return !stackItem.gone
})?.view {
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 = 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 first {
// First horizontal item has no spacing by default unless told otherwise.
pinView(view, toView: contentView, attribute: .leading, relation: .equal, priority: .required, constant: useStackSpacingBeforeFirstItem ? spacing : stackItem.spacing ?? 0)
} else if let previousView = items.last(where: { stackItem in
return !stackItem.gone
})?.view {
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 = 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)
}
func setWithStackItems(_ items: [StackItemModel]) {
removeAllItemViews()
self.items.removeAll()
var previousPresentItem: StackItemModel? = nil
for item in items {
if !item.gone {
previousPresentItem = item
}
addStackItem(item, lastItem: false)
}
if let lastView = previousPresentItem?.view {
let attribute: NSLayoutConstraint.Attribute = axis == .vertical ? .bottom : .right
pinView(contentView, toView: lastView, attribute: attribute, relation: .equal, priority: .required, constant: 0)
}
}
}