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


70

我有一个简单的水平结构UIStackView,里面堆叠了几个UIView。我的目标是在视图之间创建可变的间距。我很清楚,我可以使用“ spacing”属性在子视图之间创建恒定的空间。但是我的目标是创建可变空间。请注意,如果可能的话,我要避免使用充当间隔符的不可见视图。

我想到的最好的办法是将我包装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];

结果是一个堆栈,其中视图彼此相邻,并且间隔一定:

在此处输入图片说明

Answers:


137

更新iOS 11,具有自定义间距的StackViews

Apple已添加了在iOS 11中设置自定义间距的功能。您只需在每个排列的子视图后指定间距即可。不幸的是,您之前不能指定间距。

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

仍然比使用您自己的视图更好。

对于iOS 10及以下

您可以简单地将透明视图添加到堆栈视图中,并为其添加宽度约束。

(标签-UIView-标签-UIView -Label)

如果您继续distribution填写,则可以在UIViews上设置可变宽度约束。

但是我会考虑如果是这种情况,使用栈视图是否合适。自动布局使在视图之间设置可变宽度变得非常容易。


2
谢谢,但是我希望我不必求助于空白视图来增加空间或返回到自动布局。尽管如果UIStackView不支持我的用例,我仍然需要这样做。
yura 2015年

2
为什么要让间距可变?如果使用“分布”“等中心”,则可以具有可变的间距。这将使中心之间的宽度相等,但是间距会有所不同。详细说明您的用例。
Rob Norback 2015年

2
基本上,我有几个视图需要水平(或垂直)分布,并且在视图之间具有预定的间距,这些间距可以相同或可以不相同。我希望能够使用layoutMargins或其他某种机制来指定这些边距。
yura

让我知道是否找到另一种方法,但是我认为在stackview内的UIViews上设置约束是最直接的解决方案。
罗布·诺巴克

当然可以!谢谢你的帮助。为了澄清您的评论“我认为在stackview内的UIView上设置约束是最直接的解决方案”-您是指您最初的建议是在可见的视图之间插入空白视图吗?
yura 2015年

5

SWIFT 4

在lilpit答案之后,这是UIStackView的扩展,可为您的ScheduledSubview添加顶部和底部间距

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)
            }
        }
    }
}

然后,您将像这样使用它:

//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)

2

根据Rob的回应,我创建了一个UIStackView扩展,该扩展可能会有所帮助:

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)
      }
    }
  }
}

您可以使用,如果你想在“修改你想要的任何方式,exemplo separatorView ”的提法,你可以返回的UIView:

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

1
如果您已经在stackView中定义了间距,则此方法将不起作用(在这种情况下,ios 11版本将按预期工作,但是ios10版本将具有不同的间距(2 * defaultSpacing +间距)
lilpit

如果要具有自定义间距,则不应使用interval属性。另外,您需要使用stackView.alignment = .fill
Enrique

我不理解您的答案,使用setCustomSpacing,可以将setCustomSpacing
spacespace

1
怎么不行 您复制并粘贴了我的答案,并使用了相同的答案setCustomSpacing。另外,您将名称和地点更改为似乎不同的答案。
Enrique

0

为了支持iOS 11.x及更低版本,我像提到的Enrique一样扩展了UIStackView ,但是我对其进行了修改,使其包括:

  • 在布置的子视图之前添加一个空格
  • 处理空间已经存在并且仅需要更新的情况
  • 删除额外的空间
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()
        }
    }
}

注意:1-根据文档:

为防止在调用堆栈的removeArrangedSubview:方法后视图出现在屏幕上,请通过调用视图的removeFromSuperview()方法将其从子视图数组中显式删除,或将视图的isHidden属性设置为true。


0

要实现类似CSS边距和填充的行为。

  1. 填充

    myStackView.directionalLayoutMargins = NSDirectionalEdgeInsets(顶部:顶部,前导:左,底部:底部,尾部:右);

  2. 边距(创建包装视图并向包装添加填充)

        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);
    

0

如果您不知道前一个视图,则可以创建自己的间距UIView并将其作为已安排的子视图添加到堆栈视图中。

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))
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.