mvm_core_ui/MVMCoreUI/Molecules/MoleculeStackView.swift
Pfeil, Scott Robert ed87bd2cfc Button json to have style and size.
Separator alone in stack fix.
remove view constraining view auto background color
fix padding for stack in stack
2019-06-21 12:52:30 -04:00

268 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] = []
var useStackSpacingBeforeFirstItem = false
private var moleculesShouldSetHorizontalMargins = true
private 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() {
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?()
}
}
}
// If this item is in another container, do have the child molecules not use alignment.
public override func shouldSetHorizontalMargins(_ shouldSet: Bool) {
super.shouldSetHorizontalMargins(shouldSet)
moleculesShouldSetHorizontalMargins = false
}
public override func shouldSetVerticalMargins(_ shouldSet: Bool) {
super.shouldSetVerticalMargins(shouldSet)
moleculesShouldSetVerticalMargins = false
}
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(.fill)
}
// 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.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?(moleculesShouldSetHorizontalMargins && axis == .vertical)
(view as? MVMCoreUIViewConstrainingProtocol)?.shouldSetVerticalMargins?(moleculesShouldSetVerticalMargins)
(view as? MVMCoreUIMoleculeViewProtocol)?.setWithJSON(moleculeJSON, delegateObject: delegateObject, additionalData: nil)
}
}
}
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: useStackSpacingBeforeFirstItem ? spacing : stackItem.spacing ?? 0)
} 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: useStackSpacingBeforeFirstItem ? spacing : 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)
}
}