285 lines
12 KiB
Swift
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)
|
|
}
|
|
}
|
|
}
|