Skip to content

Instantly share code, notes, and snippets.

@DanielCardonaRojas
Last active April 5, 2023 17:48
Show Gist options
  • Save DanielCardonaRojas/fa2a00e8c50cc01021386f78ada189f9 to your computer and use it in GitHub Desktop.
Save DanielCardonaRojas/fa2a00e8c50cc01021386f78ada189f9 to your computer and use it in GitHub Desktop.
An arranged view container that allow creating all sorts of layouts even UIStackView clones.
import Foundation
import UIKit
open class ArrangedViewContainer: UIView
{
public private(set) var hiddenViews = Set<UIView>()
private var observations: Set<NSKeyValueObservation> = Set()
open private(set) var arrangedSubviews = [UIView]()
private var invalidated = false
private var viewConstraints: [NSLayoutConstraint] = []
open func addArrangedSubview(_ view: UIView)
{
insertArrangedSubview(view, atIndex: arrangedSubviews.count)
}
open func addArrangedSubviews(_ views: UIView...)
{
views.forEach { addArrangedSubview($0) }
}
open func removeArrangedSubview(_ view: UIView)
{
guard let index = arrangedSubviews.firstIndex(of: view)
else
{
return
}
view.removeObserver(self, forKeyPath: "isHidden")
arrangedSubviews.remove(at: index)
hiddenViews.remove(view)
invalidateLayout()
}
open func removeLastArrangedSubviews(_ count: Int)
{
for _ in 0 ..< count
{
guard let item = arrangedSubviews.popLast()
else
{
break
}
removeArrangedSubview(item)
}
}
open func insertArrangedSubview(_ view: UIView, atIndex stackIndex: Int)
{
if let idx = arrangedSubviews.firstIndex(of: view)
{
arrangedSubviews.remove(at: idx)
}
view.translatesAutoresizingMaskIntoConstraints = false
arrangedSubviews.insert(view, at: stackIndex)
if view.superview != self
{
addSubview(view)
}
if view.isHidden
{
setArrangedView(view, hidden: true)
}
invalidateLayout()
addVisibilityObservation(for: view)
}
private func setVisibilityObservations()
{
arrangedSubviews.forEach { addVisibilityObservation(for: $0) }
}
private func addVisibilityObservation(for view: UIView)
{
let observation = view.observe(\.isHidden, changeHandler: { _, _ in
self.setArrangedView(view, hidden: view.isHidden)
})
observations.insert(observation)
}
open func setArrangedView(_ view: UIView, hidden: Bool)
{
if hidden
{
hiddenViews.insert(view)
}
else
{
hiddenViews.remove(view)
}
invalidateLayout()
}
open func invalidateLayout()
{
if !invalidated
{
invalidated = true
setNeedsUpdateConstraints()
}
}
func isHidden(_ item: UIView) -> Bool
{
return hiddenViews.contains(item)
}
open func visibleItemLayoutConstraints(_: [UIView]) -> [NSLayoutConstraint]
{
return []
}
open func hiddenItemLayoutConstraints(_: [UIView]) -> [NSLayoutConstraint]
{
return []
}
override open func updateConstraints()
{
NSLayoutConstraint.deactivate(viewConstraints.filter { $0.isActive })
viewConstraints.removeAll()
let visibleItems = arrangedSubviews.filter { !isHidden($0) }
let visibleItemConstraints = visibleItemLayoutConstraints(visibleItems)
let hiddenItemConstraints = hiddenItemLayoutConstraints(Array(hiddenViews))
viewConstraints.append(contentsOf: visibleItemConstraints)
viewConstraints.append(contentsOf: hiddenItemConstraints)
NSLayoutConstraint.activate(viewConstraints)
super.updateConstraints()
}
}
open class VerticalListView: ArrangedViewContainer
{
open var spacing: CGFloat
{
return 8
}
override open func visibleItemLayoutConstraints(_ items: [UIView]) -> [NSLayoutConstraint]
{
var previous: UIView?
var constraints = [NSLayoutConstraint]()
for index in 0 ..< items.count
{
let item = items[index]
addSubview(item)
let itemConstraints = itemConstraints(item, previous: previous, isLast: index == items.count - 1)
constraints.append(contentsOf: itemConstraints)
previous = item
}
return constraints
}
override open func hiddenItemLayoutConstraints(_ items: [UIView]) -> [NSLayoutConstraint]
{
return items.map
{ item in
item.heightAnchor.constraint(equalToConstant: .zero)
}
}
private func itemConstraints(_ groupView: UIView, previous previousGroup: UIView?, isLast: Bool) -> [NSLayoutConstraint]
{
var itemConstraints: [NSLayoutConstraint] = []
if let previousGroup = previousGroup
{
itemConstraints.append(groupView.topAnchor.constraint(equalTo: previousGroup.bottomAnchor,
constant: spacing))
}
else
{
itemConstraints.append(groupView.topAnchor.constraint(equalTo: topAnchor))
}
itemConstraints.append(contentsOf: [groupView.leftAnchor.constraint(equalTo: leftAnchor),
groupView.rightAnchor.constraint(equalTo: rightAnchor)])
if isLast
{
itemConstraints.append(groupView.bottomAnchor.constraint(equalTo: bottomAnchor))
}
return itemConstraints
}
override open func invalidateLayout()
{
setNeedsUpdateConstraints()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment