Skip to content

Instantly share code, notes, and snippets.

@algal
Last active March 8, 2019 23:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save algal/450ed4ab78e94fd1ad1fcb68e90ecbc6 to your computer and use it in GitHub Desktop.
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.
//
// 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