mvm_core_ui/MVMCoreUI/Molecules/MoleculeStackView.swift
Pfeil, Scott Robert bbb1901be4 remove space
2019-06-18 13:35:48 -04:00

255 lines
11 KiB
Swift

//
// MoleculeStackView.swift
// MVMCoreUI
//
// Created by Scott Pfeil on 2/11/19.
// Copyright © 2019 Verizon Wireless. All rights reserved.
//
import UIKit
public class StackItem {
var view: UIView
var spacing: CGFloat?
var percentage: Int?
var verticalAlignment: UIStackView.Alignment?
var horizontalAlignment: UIStackView.Alignment?
init(with view: UIView) {
self.view = view
}
init(with view: UIView, json: [AnyHashable: Any]) {
self.view = view
update(with: json)
}
func update(with json: [AnyHashable: Any]) {
spacing = json.optionalCGFloatForKey("spacing")
percentage = json["percent"] as? Int
if let alignment = json.optionalStringForKey("verticalAlignment") {
verticalAlignment = ViewConstrainingView.getAlignmentFor(alignment, defaultAlignment: .fill)
}
if let alignment = json.optionalStringForKey("horizontalAlignment") {
horizontalAlignment = ViewConstrainingView.getAlignmentFor(alignment, defaultAlignment: .fill)
}
}
}
public class MoleculeStackView: ViewConstrainingView {
var contentView: UIView = MVMCoreUICommonViewsUtility.commonView()
var items: [StackItem] = []
/// For setting the direction of the stack
var axis: NSLayoutConstraint.Axis = .vertical {
didSet {
updateViewHorizontalDefaults = axis == .horizontal
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() {
MVMCoreUIStackableViewController.remove(contentView.subviews)
let stackItems = items
items.removeAll()
for (index, item) in stackItems.enumerated() {
addStackItem(item, lastItem: index == stackItems.count - 1)
}
}
// 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
}
translatesAutoresizingMaskIntoConstraints = false
backgroundColor = .clear
addConstrainedView(contentView)
contentView.setContentHuggingPriority(.defaultHigh, for: .vertical)
contentView.setContentHuggingPriority(.defaultHigh, for: .horizontal)
}
public override func updateView(_ size: CGFloat) {
super.updateView(size)
for item in items {
(item.view as? MVMCoreViewProtocol)?.updateView(size)
}
}
// MARK: - MVMCoreUIMoleculeViewProtocol
public override func setAsMolecule() {
updateViewHorizontalDefaults = false
}
public override func reset() {
backgroundColor = .clear
for item in items {
if let view = item.view as? MVMCoreUIMoleculeViewProtocol {
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)
MVMCoreUIStackableViewController.remove(contentView.subviews)
// If the items in the stack are the same, just update previous items instead of re-allocating.
var items: [StackItem]?
if MoleculeStackView.name(forReuse: previousJSON, delegateObject: delegateObject) == MoleculeStackView.name(forReuse: json, delegateObject: delegateObject) {
items = self.items
}
self.items = []
if let colorString = json?.optionalStringForKey(KeyBackgroundColor) {
backgroundColor = .mfGet(forHex: colorString)
}
guard let molecules = json?.arrayForKey(KeyMolecules) as? [[String: Any]] else {
return
}
// Sets the stack attributes
setAxisWithJSON(json)
spacing = json?.optionalCGFloatForKey("spacing") ?? 16
// Set the alignment for the stack in the containing view. The json driven value is for the axis direction alignment.
if axis == .vertical {
alignHorizontal(.fill)
alignVertical(ViewConstrainingView.getAlignmentFor(json?.optionalStringForKey("alignment"), defaultAlignment: .fill))
} else {
alignHorizontal(ViewConstrainingView.getAlignmentFor(json?.optionalStringForKey("alignment"), defaultAlignment: .fill))
alignVertical(.leading)
}
// Adds the molecules and sets the json.
for (index, map) in molecules.enumerated() {
if let moleculeJSON = map.optionalDictionaryForKey(KeyMolecule) {
var view: UIView?
if let item = items?[index] {
(item.view as? MVMCoreUIMoleculeViewProtocol)?.setWithJSON(moleculeJSON, delegateObject: delegateObject, additionalData: additionalData)
item.update(with: moleculeJSON)
view = item.view
addStackItem(item, lastItem: index == molecules.count - 1)
} else if let molecule = MVMCoreUIMoleculeMappingObject.shared()?.createMolecule(forJSON: moleculeJSON, delegateObject: delegateObject, constrainIfNeeded: true) {
view = molecule
addStackItem(StackItem(with: molecule, json: map), lastItem: index == molecules.count - 1)
}
(view as? MVMCoreUIViewConstrainingProtocol)?.shouldSetHorizontalMargins?(axis == .vertical)
(view as? MVMCoreUIViewConstrainingProtocol)?.shouldSetVerticalMargins?(false)
}
}
}
public override static func name(forReuse molecule: [AnyHashable : Any]?, delegateObject: MVMCoreUIDelegateObject?) -> String? {
// This will aggregate names of molecules to make an id.
var name = ""
guard let molecules = molecule?.optionalArrayForKey(KeyMolecules) else {
return name
}
for case let item as [AnyHashable: AnyHashable] in molecules {
if let moleculeName = item.stringOptionalWithChainOfKeysOrIndexes([KeyMolecule, KeyMoleculeName]) {
name.append(moleculeName)
}
}
return name
}
// MARK: - Adding to stack
/// Adds the view to the stack.
func addView(_ view: UIView, lastItem: Bool) {
addStackItem(StackItem(with: view), lastItem: lastItem)
}
/// Adds the stack item to the stack.
func addStackItem(_ stackItem: StackItem, lastItem: Bool) {
let view = stackItem.view
contentView.addSubview(view)
view.translatesAutoresizingMaskIntoConstraints = false
let spacing = stackItem.spacing ?? self.spacing
if let view = view as? MVMCoreUIViewConstrainingProtocol {
let verticalAlignment = stackItem.verticalAlignment ?? (stackItem.percentage == nil && axis == .vertical ? .fill : (axis == .vertical ? .leading : .center))
let horizontalAlignment = stackItem.horizontalAlignment ?? view.alignment?() ?? (axis == .vertical || stackItem.percentage == nil ? .fill : .leading)
view.alignHorizontal?(horizontalAlignment)
view.alignVertical?(verticalAlignment)
}
if axis == .vertical {
if items.count == 0 {
pinView(view, toView: contentView, attribute: .top, relation: .equal, priority: .required, constant: spacing)
} else if let previousView = items.last?.view {
_ = NSLayoutConstraint(pinFirstView: previousView, toSecondView: view, withConstant: spacing, directionVertical: 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 items.count == 0 {
// First horizontal item has no spacing by default unless told otherwise.
pinView(view, toView: contentView, attribute: .leading, relation: .equal, priority: .required, constant: stackItem.spacing ?? 0)
} else if let previousView = items.last?.view {
_ = NSLayoutConstraint(pinFirstView: previousView, toSecondView: view, withConstant: spacing, directionVertical: false)
}
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)
}
}