【问题标题】:How can I create UIStackView with variable spacing between views?如何在视图之间创建具有可变间距的 UIStackView?
【发布时间】: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];

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

【问题讨论】:

    标签: ios9 ios-autolayout uistackview


    【解决方案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 边距和填充。

        1. 填充

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

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

        【讨论】:

          【解决方案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))
          

          【讨论】: