To support iOS 11.x and lower, I extended the UIStackView like Enrique mentioned, however I modified it to include:
- Adding a space before the arrangedSubview
- Handling cases where a space already exists and just needs to be updated
- Removing an added space
extension UIStackView {
func addSpacing(_ spacing: CGFloat, after arrangedSubview: UIView) {
if #available(iOS 11.0, *) {
setCustomSpacing(spacing, after: arrangedSubview)
} else {
let index = arrangedSubviews.firstIndex(of: arrangedSubview)
if let index = index, arrangedSubviews.count > (index + 1), arrangedSubviews[index + 1].accessibilityIdentifier == "spacer" {
arrangedSubviews[index + 1].updateConstraint(axis == .horizontal ? .width : .height, to: spacing)
} else {
let separatorView = UIView(frame: .zero)
separatorView.accessibilityIdentifier = "spacer"
separatorView.translatesAutoresizingMaskIntoConstraints = false
switch axis {
case .horizontal:
separatorView.widthAnchor.constraint(equalToConstant: spacing).isActive = true
case .vertical:
separatorView.heightAnchor.constraint(equalToConstant: spacing).isActive = true
@unknown default:
return
}
if let index = index {
insertArrangedSubview(separatorView, at: index + 1)
}
}
}
}
func addSpacing(_ spacing: CGFloat, before arrangedSubview: UIView) {
let index = arrangedSubviews.firstIndex(of: arrangedSubview)
if let index = index, index > 0, arrangedSubviews[index - 1].accessibilityIdentifier == "spacer" {
let previousSpacer = arrangedSubviews[index - 1]
switch axis {
case .horizontal:
previousSpacer.updateConstraint(.width, to: spacing)
case .vertical:
previousSpacer.updateConstraint(.height, to: spacing)
@unknown default: return // Incase NSLayoutConstraint.Axis is extended in future
}
} else {
let separatorView = UIView(frame: .zero)
separatorView.accessibilityIdentifier = "spacer"
separatorView.translatesAutoresizingMaskIntoConstraints = false
switch axis {
case .horizontal:
separatorView.widthAnchor.constraint(equalToConstant: spacing).isActive = true
case .vertical:
separatorView.heightAnchor.constraint(equalToConstant: spacing).isActive = true
@unknown default:
return
}
if let index = index {
insertArrangedSubview(separatorView, at: max(index - 1, 0))
}
}
}
func removeSpacing(after arrangedSubview: UIView) {
if #available(iOS 11.0, *) {
setCustomSpacing(0, after: arrangedSubview)
} else {
if let index = arrangedSubviews.firstIndex(of: arrangedSubview), arrangedSubviews.count > (index + 1), arrangedSubviews[index + 1].accessibilityIdentifier == "spacer" {
arrangedSubviews[index + 1].removeFromStack()
}
}
}
func removeSpacing(before arrangedSubview: UIView) {
if let index = arrangedSubviews.firstIndex(of: arrangedSubview), index > 0, arrangedSubviews[index - 1].accessibilityIdentifier == "spacer" {
arrangedSubviews[index - 1].removeFromStack()
}
}
}
extension UIView {
func updateConstraint(_ attribute: NSLayoutConstraint.Attribute, to constant: CGFloat) {
for constraint in constraints {
if constraint.firstAttribute == attribute {
constraint.constant = constant
}
}
}
func removeFromStack() {
if let stack = superview as? UIStackView, stack.arrangedSubviews.contains(self) {
stack.removeArrangedSubview(self)
// Note: 1
removeFromSuperview()
}
}
}
Note: 1 - According to the documentation:
To prevent the view from appearing on screen after calling the stack’s
removeArrangedSubview: method, explicitly remove the view from the
subviews array by calling the view’s removeFromSuperview() method, or
set the view’s isHidden property to true.