Last active
March 8, 2019 23:19
-
-
Save algal/450ed4ab78e94fd1ad1fcb68e90ecbc6 to your computer and use it in GitHub Desktop.
Like a simple vertical stack view. No animations. But no magical mystery meat, and it works.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// Pile.swift | |
// PileTest | |
// | |
// Created by Alexis Gallagher on 3/6/19. | |
// Copyright © 2019 Bespoke. All rights reserved. | |
// | |
// known-good: Swift 4.2, iOS 12 | |
import Foundation | |
import UIKit | |
/** | |
Like UIStackView. But does not provide animations and does not change the behavior of `isHidden`. | |
Specifically, this mimics a stack view defined like so: | |
``` | |
stackView.axis = .vertical | |
stackView.alignment = .fill | |
stackView.distribution = .fill | |
stackView.spacing = 0 | |
``` | |
By default this view sets its own `layoutMargins` to `UIEdgeInsets.zero` by default. Set another value if you want margins around the piled views. This view is reasonably performant. It only creates and destroys constraints as needed. | |
(I wrote this because I found `UIStackView` to be unstable in complex scenarios, but I didn't want to refactor complex code that already dependend on it.) | |
*/ | |
class PileView: UIView | |
{ | |
/// On-axis constraints we assign to an arranged view | |
private struct ManagedConstraints { | |
var top:NSLayoutConstraint | |
var bottom:NSLayoutConstraint | |
var constraints:[NSLayoutConstraint] { | |
return [top,bottom].compactMap({ $0 }) | |
} | |
} | |
/// Bookkeeping of constraints this view manages, so as not to interfere with other constraints | |
private var arrangedConstraints:[UIView:ManagedConstraints] = [:] | |
/// The list of views arranged by the pile view | |
internal private(set) var arrangedSubviews:[UIView] = [] | |
override init(frame: CGRect) { | |
super.init(frame: frame) | |
self.layoutMargins = UIEdgeInsets.zero | |
self.insetsLayoutMarginsFromSafeArea = false | |
} | |
required init?(coder aDecoder: NSCoder) { | |
fatalError("unimplemented") | |
} | |
/// Removes the provided view from the pile’s array of arranged subviews. | |
func removeArrangedSubview(_ view:UIView) | |
{ | |
guard let i = arrangedSubviews.firstIndex(of: view) else { return } | |
// get the views which will be above and below the view we are inserting. | |
// if nil, that means the neighboring view is self | |
let aboveView = ( i == arrangedSubviews.startIndex ) ? nil : arrangedSubviews[i - 1] | |
let belowView = ( i == arrangedSubviews.endIndex.advanced(by: -1) ) ? nil : arrangedSubviews[i + 1] | |
let constraintToActivate:NSLayoutConstraint? | |
switch (aboveView,belowView) { | |
case (nil,nil): | |
// we're the only view | |
// no constraints to add | |
constraintToActivate = nil | |
break | |
case (.some(let theAboveView),nil): | |
// we're at the bottom | |
// attach our aboveView to the container | |
let aboveViewToContainerBottom = theAboveView.bottomAnchor.constraint(equalTo: self.layoutMarginsGuide.bottomAnchor) | |
arrangedConstraints[theAboveView]?.bottom = aboveViewToContainerBottom | |
constraintToActivate = aboveViewToContainerBottom | |
case (nil,.some(let theBelowView)): | |
// we're at the top | |
// attach our belowView to the container | |
let belowViewToContainerTop = theBelowView.topAnchor.constraint(equalTo: self.layoutMarginsGuide.topAnchor) | |
arrangedConstraints[theBelowView]?.top = belowViewToContainerTop | |
constraintToActivate = belowViewToContainerTop | |
case (.some(let theAboveView),.some(let theBelowView)): | |
// we're in the middle | |
// attach our aboveView to our belowView | |
let aboveViewToBelowView = theAboveView.bottomAnchor.constraint(equalTo: theBelowView.topAnchor) | |
arrangedConstraints[theAboveView]?.bottom = aboveViewToBelowView | |
arrangedConstraints[theBelowView]?.top = aboveViewToBelowView | |
constraintToActivate = aboveViewToBelowView | |
} | |
// remove ourselves | |
self.arrangedConstraints.removeValue(forKey: view) | |
self.arrangedSubviews.remove(at: i) | |
view.removeFromSuperview() | |
constraintToActivate?.isActive = true | |
} | |
/// Adds the provided view to the array of arranged subviews at the specified index. | |
func insertArrangedSubview(_ view:UIView, at i: Int) | |
{ | |
guard arrangedSubviews.contains(view) == false else { return } | |
// get the neightboring views which will be above and below the view we are inserting. | |
// if nil, that means the neighboring view is self | |
let aboveView = ( i == arrangedSubviews.startIndex ) ? nil : arrangedSubviews[i - 1] | |
let belowView = ( i == arrangedSubviews.endIndex ) ? nil : arrangedSubviews[i] | |
arrangedSubviews.insert(view, at: i) | |
view.translatesAutoresizingMaskIntoConstraints = false | |
self.addSubview(view) | |
// new constraints to add | |
let vm:ManagedConstraints | |
switch (aboveView,belowView) | |
{ | |
case (nil,nil): | |
// container is empty. | |
// the new view the first view. we constrain to the container on all edges | |
vm = ManagedConstraints(top: self.layoutMarginsGuide.topAnchor.constraint(equalTo: view.topAnchor), | |
bottom: self.layoutMarginsGuide.bottomAnchor.constraint(equalTo: view.bottomAnchor)) | |
case (nil,.some(let theBelowView)): | |
// new view is being inserted at 0, between the container and an existing view | |
// so we replace the existing view's container link with a link to the new view | |
arrangedConstraints[theBelowView]?.top.isActive = false | |
let viewToBelowView = view.bottomAnchor.constraint(equalTo: theBelowView.topAnchor) | |
arrangedConstraints[theBelowView]?.top = viewToBelowView | |
// and constrain the new view to to the container | |
let viewToContainerTop = view.topAnchor.constraint(equalTo: self.layoutMarginsGuide.topAnchor) | |
vm = ManagedConstraints(top:viewToContainerTop, bottom: viewToBelowView) | |
case (.some(let theAboveView),nil): | |
// new view is being inserted last, between an existing view and the container | |
// so we replace the existing view's container link with a link to the new view | |
arrangedConstraints[theAboveView]?.bottom.isActive = false | |
let viewToAboveView = view.topAnchor.constraint(equalTo: theAboveView.bottomAnchor) | |
arrangedConstraints[theAboveView]?.bottom = viewToAboveView | |
// and constrain the new view to to the container | |
let viewToContainerBottom = view.bottomAnchor.constraint(equalTo: self.layoutMarginsGuide.bottomAnchor) | |
vm = ManagedConstraints(top: viewToAboveView, bottom: viewToContainerBottom) | |
case (.some(let theAboveView),.some(let theBelowView)): | |
// new view is being inserted between two existing views | |
// so we detach their links to each other | |
arrangedConstraints[theAboveView]?.bottom.isActive = false | |
arrangedConstraints[theBelowView]?.top.isActive = false | |
// link to the new view instead | |
let viewToAboveView = view.topAnchor.constraint(equalTo: theAboveView.bottomAnchor) | |
let viewToBelowView = view.bottomAnchor.constraint(equalTo: theBelowView.topAnchor) | |
arrangedConstraints[theAboveView]?.bottom = viewToAboveView | |
arrangedConstraints[theBelowView]?.top = viewToBelowView | |
vm = ManagedConstraints(top: viewToAboveView, bottom: viewToBelowView) | |
} | |
arrangedConstraints[view] = vm | |
var cs = vm.constraints | |
cs.append(self.leftAnchor.constraint(equalTo: view.leftAnchor)) | |
cs.append(self.rightAnchor.constraint(equalTo: view.rightAnchor)) | |
NSLayoutConstraint.activate(cs) | |
} | |
/// Adds a view to the end of the arrangedSubviews array. | |
func addArrangedSubview(_ view:UIView) | |
{ | |
self.insertArrangedSubview(view, at: self.arrangedSubviews.endIndex) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment