如何创建视图之间具有可变间距的 UIStackView?

我有一个简单的水平 UIStackView与几个 UIView 堆叠内。我的目标是在视图之间创建可变的间距。我很清楚我可以使用“间距”属性在子视图之间创建常量空间。然而,我的目标是创建可变空间。请注意,如果可能的话,我希望避免使用作为间隔的不可见视图。

我想到的最好的办法是将我的 UIViews封装在一个单独的 UIStackView中,并使用 layoutMarginsRelativeArrangement = YES来尊重内部堆栈的布局边距。我希望我可以做类似的事情与任何 UIView不诉诸于这个丑陋的解决办法。下面是我的示例代码:

// Create stack view
UIStackView *stackView = [[UIStackView alloc] init];
stackView.translatesAutoresizingMaskIntoConstraints = NO;
stackView.axis = UILayoutConstraintAxisHorizontal;
stackView.alignment = UIStackViewAlignmentCenter;
stackView.layoutMarginsRelativeArrangement = YES;


// Create subview
UIView *view1 = [[UIView alloc] init];
view1.translatesAutoresizingMaskIntoConstraints = NO;
// ... Add Auto Layout constraints for height / width
// ...
// I was hoping the layoutMargins would be respected, but they are not
view1.layoutMargins = UIEdgeInsetsMake(0, 25, 0, 0);


// ... Create more subviews
// UIView view2 = [[UIView alloc] init];
// ...


// Stack the subviews
[stackView addArrangedSubview:view1];
[stackView addArrangedSubview:view2];

其结果是一个视图堆栈,视图间隔相邻:

enter image description here

58966 次浏览

Update For iOS 11, StackViews with Custom Spacing

Apple has added the ability to set custom spacing in iOS 11. You simply have to specify the spacing after each arranged subview. Unfortunately you can't specify spacing before.

stackView.setCustomSpacing(10.0, after: firstLabel)
stackView.setCustomSpacing(10.0, after: secondLabel)

Still way better than using your own views.

For iOS 10 and Below

You could simply add a transparent views into your stack view and add width constraints to them.

(Label - UIView - Label - UIView -Label)

and if you keep distribution to fill, then you can setup variable width constraints on your UIViews.

But I would consider if this is the right situation to use stackviews if that's the case. Autolayout makes it very easy to setup variable widths between views.

From Rob's response I created a UIStackView extension that might help:

extension UIStackView {
func addCustomSpacing(_ spacing: CGFloat, after arrangedSubview: UIView) {
if #available(iOS 11.0, *) {
self.setCustomSpacing(spacing, after: arrangedSubview)
} else {
let separatorView = UIView(frame: .zero)
separatorView.translatesAutoresizingMaskIntoConstraints = false
switch axis {
case .horizontal:
separatorView.widthAnchor.constraint(equalToConstant: spacing).isActive = true
case .vertical:
separatorView.heightAnchor.constraint(equalToConstant: spacing).isActive = true
}
if let index = self.arrangedSubviews.firstIndex(of: arrangedSubview) {
insertArrangedSubview(separatorView, at: index + 1)
}
}
}
}

You can use and modify it any way you want, for exemplo if you want the "separatorView" reference, you can just return the UIView:

  func addCustomSpacing(_ spacing: CGFloat, after arrangedSubview: UIView) -> UIView?

SWIFT 4

Following lilpit answer, here is an extension of the UIStackView to add a top and a bottom spacing to your arrangedSubview

extension UIStackView {
func addCustomSpacing(top: CGFloat, bottom: CGFloat) {


//If the stack view has just one arrangedView, we add a dummy one
if self.arrangedSubviews.count == 1 {
self.insertArrangedSubview(UIView(frame: .zero), at: 0)
}


//Getting the second last arrangedSubview and the current one
let lastTwoArrangedSubviews = Array(self.arrangedSubviews.suffix(2))
let arrSpacing: [CGFloat] = [top, bottom]


//Looping through the two last arrangedSubview to add spacing in each of them
for (index, anArrangedSubview) in lastTwoArrangedSubviews.enumerated() {


//After iOS 11, the stackview has a native method
if #available(iOS 11.0, *) {
self.setCustomSpacing(arrSpacing[index], after: anArrangedSubview)
//Before iOS 11 : Adding dummy separator UIViews
} else {
guard let arrangedSubviewIndex = arrangedSubviews.firstIndex(of: anArrangedSubview) else {
return
}


let separatorView = UIView(frame: .zero)
separatorView.translatesAutoresizingMaskIntoConstraints = false


//calculate spacing to keep a coherent spacing with the ios11 version
let isBetweenExisitingViews = arrangedSubviewIndex != arrangedSubviews.count - 1
let existingSpacing = isBetweenExisitingViews ? 2 * spacing : spacing
let separatorSize = arrSpacing[index] - existingSpacing


guard separatorSize > 0 else {
return
}


switch axis {
case .horizontal:
separatorView.widthAnchor.constraint(equalToConstant: separatorSize).isActive = true
case .vertical:
separatorView.heightAnchor.constraint(equalToConstant: separatorSize).isActive = true
}


insertArrangedSubview(separatorView, at: arrangedSubviewIndex + 1)
}
}
}
}

Then you would use it like this:

//Creating label to add to the UIStackview
let label = UILabel(frame: .zero)


//Adding label to the UIStackview
stackView.addArrangedSubview(label)


//Create margin on top and bottom of the UILabel
stackView.addCustomSpacing(top: 40, bottom: 100)

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.

To achieve a similar behavior like CSS margin and padding.

  1. Padding

    myStackView.directionalLayoutMargins = NSDirectionalEdgeInsets(top: top, leading: left, bottom: bottom, trailing: right);

  2. Margin (create a wrapper View and add padding to the wrapper)

        wrapper = UIStackView();
    wrapper!.frame = viewToAdd.frame;
    wrapper!.frame.size.height = wrapper!.frame.size.height + marginTop + marginBottom;
    wrapper!.frame.size.width = wrapper!.frame.size.width + marginLeft + marginRight;
    (wrapper! as! UIStackView).axis = .horizontal;
    (wrapper! as! UIStackView).alignment = .fill
    (wrapper! as! UIStackView).spacing = 0
    (wrapper! as! UIStackView).distribution = .fill
    wrapper!.translatesAutoresizingMaskIntoConstraints = false
    
    
    (wrapper! as! UIStackView).isLayoutMarginsRelativeArrangement = true;
    (wrapper! as! UIStackView).insetsLayoutMarginsFromSafeArea = false;
    wrapper!.directionalLayoutMargins = NSDirectionalEdgeInsets(top: marginTop, leading: marginLeft, bottom: marginBottom, trailing: marginRight);wrapper.addArrangedSubview(viewToAdd);
    

If you do not know the previous view you can create your own spacing UIView and add it to your stack view as an arranged subview.

func spacing(value: CGFloat) -> UIView {
let spacerView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: 0))
spacerView.translatesAutoresizingMaskIntoConstraints = false
spacerView.heightAnchor.constraint(equalToConstant: value).isActive = true
return spacerView
}
stackView.addArrangedSubview(spacing(value: 16))