Skip to content

Instantly share code, notes, and snippets.

@vmanot
Last active April 6, 2023 20:36
Show Gist options
  • Save vmanot/7d9ea7df2658d9e1358ae6a877fefd7e to your computer and use it in GitHub Desktop.
Save vmanot/7d9ea7df2658d9e1358ae6a877fefd7e to your computer and use it in GitHub Desktop.
A Better `UIStackView`
//
// Copyright (c) Vatsal Manot
//
#if canImport(UIKit)
import Swift
import UIKit
///
/// A modern `UIStackView` replacement.
///
open class UIModernStackView<ArrangedSubview: UIView>: UIStackView {
override open var axis: NSLayoutConstraint.Axis {
didSet {
super.invalidateIntrinsicContentSize()
}
}
override open var distribution: Distribution {
didSet {
super.invalidateIntrinsicContentSize()
}
}
override open var alignment: Alignment {
didSet {
super.invalidateIntrinsicContentSize()
}
}
override open var spacing: CGFloat {
didSet {
super.invalidateIntrinsicContentSize()
}
}
override open var intrinsicContentSize: CGSize {
var size = CGSize.zero
var hasSubviewWithNoIntrinsicWidth: Bool = false
var hasSubviewWithNoIntrinsicHeight: Bool = false
for view in arrangedSubviews {
let intrinsicContentSize = view.intrinsicContentSize
if intrinsicContentSize.width == UIView.noIntrinsicMetric {
hasSubviewWithNoIntrinsicWidth = true
}
if intrinsicContentSize.height == UIView.noIntrinsicMetric {
hasSubviewWithNoIntrinsicHeight = true
}
if hasSubviewWithNoIntrinsicWidth && hasSubviewWithNoIntrinsicHeight {
return super.intrinsicContentSize
}
switch axis {
case .horizontal:
size.width += intrinsicContentSize.width
size.height = max(size.height, intrinsicContentSize.height)
case .vertical:
size.width = max(size.width, intrinsicContentSize.width)
size.height += intrinsicContentSize.height
}
}
if arrangedSubviews.count != 0 {
let totalSpacing = .init(arrangedSubviews.count - 1) * spacing
switch axis {
case .horizontal:
size.width += totalSpacing
case .vertical:
size.height += totalSpacing
}
}
if hasSubviewWithNoIntrinsicWidth {
size.width = UIView.noIntrinsicMetric
}
if hasSubviewWithNoIntrinsicHeight {
size.height = UIView.noIntrinsicMetric
}
return size
}
init() {
super.init(frame: .zero)
}
public required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override open func invalidateIntrinsicContentSize() {
super.invalidateIntrinsicContentSize()
for view in arrangedSubviews {
view.invalidateIntrinsicContentSize()
}
}
override open func layoutSubviews() {
super.layoutSubviews()
super.invalidateIntrinsicContentSize()
}
override open func setContentHuggingPriority(_ priority: UILayoutPriority, for axis: NSLayoutConstraint.Axis) {
super.setContentHuggingPriority(priority, for: axis)
for view in arrangedSubviews {
view.setContentHuggingPriority(priority, for: axis)
}
super.invalidateIntrinsicContentSize()
}
open var contentHuggingPriority: UILayoutPriority {
get {
return contentHuggingPriority(for: axis)
} set {
setContentHuggingPriority(newValue, for: axis)
}
}
override open func setContentCompressionResistancePriority(_ priority: UILayoutPriority, for axis: NSLayoutConstraint.Axis) {
super.setContentCompressionResistancePriority(priority, for: axis)
for view in arrangedSubviews {
view.setContentCompressionResistancePriority(priority, for: axis)
}
super.invalidateIntrinsicContentSize()
}
open var contentCompressionResistancePriority: UILayoutPriority {
get {
return contentCompressionResistancePriority(for: axis)
} set {
setContentCompressionResistancePriority(newValue, for: axis)
}
}
override open func didAddSubview(_ subview: UIView) {
super.didAddSubview(subview)
super.invalidateIntrinsicContentSize()
setNeedsLayout()
}
@available(*, deprecated)
override open func insertArrangedSubview(_ view: UIView, at stackIndex: Int) {
precondition(view is ArrangedSubview)
super.insertArrangedSubview(view, at: stackIndex)
}
open func insertArrangedSubview(_ view: ArrangedSubview, at stackIndex: Int) {
super.insertArrangedSubview(view, at: stackIndex)
}
@available(*, deprecated)
override open func addArrangedSubview(_ view: UIView) {
precondition(view is ArrangedSubview)
super.addArrangedSubview(view)
}
open func addArrangedSubview(_ view: ArrangedSubview) {
super.addArrangedSubview(view)
}
@available(*, deprecated)
override open func removeArrangedSubview(_ view: UIView) {
precondition(view is ArrangedSubview)
super.removeArrangedSubview(view)
}
open func removeArrangedSubview(_ view: ArrangedSubview) {
super.removeArrangedSubview(view)
}
}
extension UIModernStackView {
public static func horizontal(
_ arrangedSubviews: [ArrangedSubview],
distribution: UIStackView.Distribution = .equalSpacing,
alignment: UIStackView.Alignment = .center,
spacing: CGFloat = 0) -> UIModernStackView<ArrangedSubview> {
let result = UIModernStackView<ArrangedSubview>()
result.axis = .horizontal
result.distribution = distribution
result.alignment = alignment
result.spacing = spacing
arrangedSubviews.forEach(result.addArrangedSubview)
return result
}
/// Returns a vertical stack view whose parameters can either be specified or left to defaults.
public static func vertical(
_ arrangedSubviews: [ArrangedSubview],
distribution: UIStackView.Distribution = .equalSpacing,
alignment: UIStackView.Alignment = .center,
spacing: CGFloat = 0) -> UIModernStackView<ArrangedSubview> {
let result = UIModernStackView<ArrangedSubview>()
result.axis = .vertical
result.distribution = distribution
result.alignment = alignment
result.spacing = spacing
arrangedSubviews.forEach(result.addArrangedSubview)
return result
}
}
#endif
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment