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

Posted

技术标签:

【中文标题】如何在视图之间创建具有可变间距的 UIStackView?【英文标题】:How can I create UIStackView with variable spacing between views? 【发布时间】:2016-01-05 02:15:19 【问题描述】:

我有一个简单的水平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];

结果是一个视图的堆栈,它们之间有间距:

【问题讨论】:

【参考方案1】:

针对 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 上设置可变宽度约束。

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

【讨论】:

谢谢,但我希望不必求助于空白视图来添加空间或返回自动布局。虽然如果 UIStackView 不支持我的用例,我可能仍然需要。 为什么要让间距可变?如果您使用“分布”“等中心”,则可以有可变间距。这将使中心的宽度相等,但间距会有所不同。详细说明您的用例。 基本上,我有几个视图需要水平(或垂直)分布,视图之间有预定的空间,它们的值可能相同也可能不同。我希望能够使用 layoutMargins 或其他机制指定这些边距。 如果您找到其他方法,请告诉我,但我相信在 stackview 中对 UIView 设置约束是最直接的解决方案。 当然可以!谢谢你的帮助。为了澄清您的评论“我相信在 stackview 中对 UIViews 设置约束是最直接的解决方案” - 您是指在可见视图之间插入空白视图的原始建议吗?【参考方案2】:

SWIFT 4

按照 lilpit 的回答,这里是 UIStackView 的扩展,用于为您的排列子视图添加顶部和底部间距

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)

【讨论】:

【参考方案3】:

来自Rob's 的回复,我创建了一个可能有帮助的 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)
      
    
  


您可以随意使用和修改它,例如,如果您想要“separatorView”引用,则只需返回 UIView:

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

【讨论】:

如果您已经在 stackView 中定义了间距,这将不起作用(在这种情况下,ios 11 版本将按预期工作,但 ios10 版本将具有不同的间距(2 * defaultSpacing + spacing ) 如果你想有一个自定义的间距,你不应该使用间距属性。另外,您需要使用stackView.alignment = .fill 看不懂你的回答,用setCustomSpacing,没用setCustomSpacing的索引可以用spacing属性,所以我的回答是对的 怎么样?您复制并粘贴了我的答案并使用了相同的setCustomSpacing。此外,您将地点的名称和事物更改为似乎不同的答案。【参考方案4】:

为了支持 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。

【讨论】:

【参考方案5】:

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

    填充

    myStackView.directionalLayoutMargins = NSDirectionalEdgeInsets(top: top, 前导:左,下:下,尾随:右);

    边距(创建一个包装器视图并为包装器添加填充)

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

【讨论】:

【参考方案6】:

如果您不知道之前的视图,您可以创建自己的间距 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))

【讨论】:

以上是关于如何在视图之间创建具有可变间距的 UIStackView?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 SwiftUI 中删除主要内容视图和标签栏之间的间距?

Swift - 如何在水平 StackView 中设置可变数量按钮之间的间距?

具有动态指定视图之间间距的自动布局

UIStackView 孩子等间距(周围和之间)

UICollectionView 中具有可变项目大小的恒定行距

具有相等项目间距的自定义 UICollectionViewLayout