【问题标题】:iOS - Add vertical line programatically inside a stack viewiOS - 在堆栈视图中以编程方式添加垂直线
【发布时间】:2017-10-21 06:03:29
【问题描述】:

我正在尝试以编程方式在堆栈视图内的标签之间添加垂直线。

所需的完成将类似于此图像:

我可以添加标签,所有标签都具有所需的间距;我可以添加水平线,但我不知道如何在中间添加那些分隔符垂直线。

我想这样做:

let stackView = UIStackView(arrangedSubviews: [label1, verticalLine, label2, verticalLine, label3])

有什么提示吗?

谢谢

【问题讨论】:

  • 用纯黑色创建一个 2 点宽的“UIView”(通过约束强制执行),并将其中两个添加到“UIStackView”的正确位置?

标签: ios uiview swift3 uistackview


【解决方案1】:

您不能在两个地方使用相同的视图,因此您需要创建两个单独的垂直线视图。您需要像这样配置每个垂直线视图:

  • 设置其背景颜色。
  • 将其宽度限制为 1(这样您会得到一条线,而不是矩形)。
  • 限制其高度(使其不会拉伸到堆栈视图的整个高度)。

因此,一次将一个标签添加到堆栈视图,并在将每个标签添加到堆栈视图之前执行以下操作:

if stackView.arrangedSubviews.count > 0 {
    let separator = UIView()
    separator.widthAnchor.constraint(equalToConstant: 1).isActive = true
    separator.backgroundColor = .black
    stackView.addArrangedSubview(separator)
    separator.heightAnchor.constraint(equalTo: stackView.heightAnchor, multiplier: 0.6).isActive = true
}

请注意,您确实希望垂直线与标签的宽度相同,因此您必须将堆栈视图的 distribution 属性设置为fillEqually。相反,如果您希望所有标签具有相同的宽度,您必须自己在标签之间创建宽度约束。例如,添加每个新标签后,执行以下操作:

if let firstLabel = stackView.arrangedSubviews.first as? UILabel {
    label.widthAnchor.constraint(equalTo: firstLabel.widthAnchor).isActive = true
}

结果:

完整的游乐场代码(由 Federico Zanetello 更新到 Swift 4.1):

import UIKit
import PlaygroundSupport

extension UIFont {
  var withSmallCaps: UIFont {
    let feature: [UIFontDescriptor.FeatureKey: Any] = [
      UIFontDescriptor.FeatureKey.featureIdentifier: kLowerCaseType,
      UIFontDescriptor.FeatureKey.typeIdentifier: kLowerCaseSmallCapsSelector]
    let attributes: [UIFontDescriptor.AttributeName: Any] = [UIFontDescriptor.AttributeName.featureSettings: [feature]]
    let descriptor = self.fontDescriptor.addingAttributes(attributes)
    return UIFont(descriptor: descriptor, size: pointSize)
  }
}

let rootView = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 44))
rootView.backgroundColor = .white
PlaygroundPage.current.liveView = rootView

let stackView = UIStackView()
stackView.axis = .horizontal
stackView.alignment = .center
stackView.frame = rootView.bounds
rootView.addSubview(stackView)

typealias Item = (name: String, value: Int)
let items: [Item] = [
  Item(name: "posts", value: 135),
  Item(name: "followers", value: 6347),
  Item(name: "following", value: 328),
]

let valueStyle: [NSAttributedStringKey: Any] = [NSAttributedStringKey.font: UIFont.boldSystemFont(ofSize: 12).withSmallCaps]
let nameStyle: [NSAttributedStringKey: Any] = [NSAttributedStringKey.font: UIFont.systemFont(ofSize: 12).withSmallCaps,
                                NSAttributedStringKey.foregroundColor: UIColor.darkGray]
let valueFormatter = NumberFormatter()
valueFormatter.numberStyle = .decimal

for item in items {
  if stackView.arrangedSubviews.count > 0 {
    let separator = UIView()
    separator.widthAnchor.constraint(equalToConstant: 1).isActive = true
    separator.backgroundColor = .black
    stackView.addArrangedSubview(separator)
    separator.heightAnchor.constraint(equalTo: stackView.heightAnchor, multiplier: 0.4).isActive = true
  }

  let richText = NSMutableAttributedString()
  let valueString = valueFormatter.string(for: item.value)!
  richText.append(NSAttributedString(string: valueString, attributes: valueStyle))
  richText.append(NSAttributedString(string: "\n" + item.name, attributes: nameStyle))
  let label = UILabel()
  label.attributedText = richText
  label.textAlignment = .center
  label.numberOfLines = 0
  stackView.addArrangedSubview(label)

  if let firstLabel = stackView.arrangedSubviews.first as? UILabel {
    label.widthAnchor.constraint(equalTo: firstLabel.widthAnchor).isActive = true
  }
}

UIGraphicsBeginImageContextWithOptions(rootView.bounds.size, true, 1)
rootView.drawHierarchy(in: rootView.bounds, afterScreenUpdates: true)
let image = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
let png = UIImagePNGRepresentation(image)!
let path = NSTemporaryDirectory() + "/image.png"
Swift.print(path)
try! png.write(to: URL(fileURLWithPath: path))

【讨论】:

  • 这是一个很棒的 hack,以便如何实现我想要的设计;
  • 我也遇到过类似的问题,而且这种技术效果很好!一种优化可能是添加标签约束跳过第一个。
【解决方案2】:

您可以尝试以下方法。

  1. 首先获取一个 UIView 并将 UIStackView 的相同约束应用于此 UIView。
  2. 将此 UIView 的背景颜色设置为黑色(线条的颜色)
  3. 现在获取一个 UIStackView 并将其添加为上述 UIView 的子项。
  4. 添加 UIStackView 的约束,即将其绑定到父 UIView 的所有边缘。
  5. 现在将 UIStackView 的背景颜色设置为 Clear Color。
  6. 将 UIStackView 的间距设置为 1 或 2(行宽)
  7. 现在将 3 个标签添加到 stackview 中。
  8. 确保标签的背景颜色为白色,文本颜色为黑色。

这样您就可以实现所需的场景。请参阅这些图片以供参考。

【讨论】:

  • 嗯.. 我从来没有想过这样做。它可能会解决问题。我稍后会试一试,如果它工作正常,我会反馈。谢谢
  • 这正是我要找的!
  • 一个很好的解决方案:只需使用 background-color 创建一个背景视图
  • 好主意... ;))
  • 好主意!干得好
【解决方案3】:

这是在每行之间添加分隔符的简单扩展(注意!行,而不是所要求的列!对于这种情况也很容易修改)。与接受的答案基本相同,但采用可重复使用的格式。

通过调用例如使用

yourStackViewObjectInstance.addHorizontalSeparators(color : .black)

扩展名:

extension UIStackView {
    func addHorizontalSeparators(color : UIColor) {
        var i = self.arrangedSubviews.count
        while i >= 0 {
            let separator = createSeparator(color: color)
            insertArrangedSubview(separator, at: i)
            separator.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: 1).isActive = true
            i -= 1
        }
    }

    private func createSeparator(color : UIColor) -> UIView {
        let separator = UIView()
        separator.heightAnchor.constraint(equalToConstant: 1).isActive = true
        separator.backgroundColor = color
        return separator
    }
}

【讨论】:

  • 这个不错。感谢分享
  • 效果很好。谢谢!
  • 对于阅读本文的任何人,如果您不想在顶部使用分隔符,请将 while i >= 0 更改为 while i > 0。如果您不想在底部使用分隔符,请将var i = self.arrangedSubviews.count 更改为var i = self.arrangedSubviews.count - 1
【解决方案4】:

这是一个更灵活的UIStackView子类,它支持任意添加排列的子视图,适合那些需要在UIStackView和子视图上放置在UIVisualEffectView之上的清晰背景的人,如下图所示.

import UIKit

@IBDesignable class SeparatorStackView: UIStackView {

    @IBInspectable var separatorColor: UIColor? = .black {
        didSet {
            invalidateSeparators()
        }
    }
    @IBInspectable var separatorWidth: CGFloat = 0.5 {
        didSet {
            invalidateSeparators()
        }
    }
    @IBInspectable private var separatorTopPadding: CGFloat = 0 {
        didSet {
            separatorInsets.top = separatorTopPadding
        }
    }
    @IBInspectable private var separatorBottomPadding: CGFloat = 0 {
        didSet {
            separatorInsets.bottom = separatorBottomPadding
        }
    }
    @IBInspectable private var separatorLeftPadding: CGFloat = 0 {
        didSet {
            separatorInsets.left = separatorLeftPadding
        }
    }
    @IBInspectable private var separatorRightPadding: CGFloat = 0 {
        didSet {
            separatorInsets.right = separatorRightPadding
        }
    }

    var separatorInsets: UIEdgeInsets = .zero {
        didSet {
            invalidateSeparators()
        }
    }

    private var separators: [UIView] = []

    override func layoutSubviews() {
        super.layoutSubviews()

        invalidateSeparators()
    }

    override func awakeFromNib() {
        super.awakeFromNib()

        invalidateSeparators()
    }

    override func prepareForInterfaceBuilder() {
        super.prepareForInterfaceBuilder()

        invalidateSeparators()
    }


    private func invalidateSeparators() {
        guard arrangedSubviews.count > 1 else {
            separators.forEach({$0.removeFromSuperview()})
            separators.removeAll()
            return
        }

        if separators.count > arrangedSubviews.count {
            separators.removeLast(separators.count - arrangedSubviews.count)
        } else if separators.count < arrangedSubviews.count {
            separators += Array<UIView>(repeating: UIView(), count: arrangedSubviews.count - separators.count)
        }

        separators.forEach({$0.backgroundColor = self.separatorColor; self.addSubview($0)})

        for (index, subview) in arrangedSubviews.enumerated() where arrangedSubviews.count >= index + 2 {
            let nextSubview = arrangedSubviews[index + 1]
            let separator = separators[index]

            let origin: CGPoint
            let size: CGSize

            if axis == .horizontal {
                let originX = (nextSubview.frame.maxX - subview.frame.minX)/2 + separatorInsets.left - separatorInsets.right
                origin = CGPoint(x: originX, y: separatorInsets.top)
                let height = frame.height - separatorInsets.bottom - separatorInsets.top
                size = CGSize(width: separatorWidth, height: height)
        } else {
                let originY = (nextSubview.frame.maxY - subview.frame.minY)/2 + separatorInsets.top - separatorInsets.bottom
                origin = CGPoint(x: separatorInsets.left, y: originY)
                let width = frame.width - separatorInsets.left - separatorInsets.right
                size = CGSize(width: width, height: separatorWidth)
            }

            separator.frame = CGRect(origin: origin, size: size)
        }
    }
}

结果?

【讨论】:

  • 在具有多个项目的垂直 StackView 上,这只会在第一个和第二个项目之间添加一个分隔符
  • invalidateSeparators() 中创建分隔符数组的行多次将相同的分隔符添加到数组中。
  • 它还会为第一个以外的任何分隔符计算错误的帧。正在修复。
  • 我在多个项目中都有stackView,为什么只显示第一个?
【解决方案5】:

如果垂直线字符“|”适用于您想要的外观,那么您可以将标签添加到您想要分隔线的堆栈视图中。然后使用:

myStackView.distribution = .equalSpacing

您还可以在 Interface Builder 中更改堆栈视图分布。

【讨论】:

    【解决方案6】:

    @GOR 回答扩展仅适用于垂直线且仅在中心

    Stackview 设置:设置每个子视图的宽度约束,父 stackview 应该被填充

    这是在每行之间添加垂直分隔符的简单扩展。

    func addVerticalSeparators(color : UIColor) {
        var i = self.arrangedSubviews.count
        while i > 1 {
            let separator = verticalCreateSeparator(color: color)
            insertArrangedSubview(separator, at: i-1)   // (i-1) for centers only
            separator.heightAnchor.constraint(equalTo: self.heightAnchor, multiplier: 1).isActive = true
            i -= 1
        }
    }
    
    private func verticalCreateSeparator(color : UIColor) -> UIView {
        let separator = UIView()
        separator.widthAnchor.constraint(equalToConstant: 1).isActive = true
        separator.backgroundColor = color
        return separator
    } 
    

    【讨论】:

    • 它会抛出一个错误。垂直分隔符不应与 UIStackView 具有相同的高度。
    【解决方案7】:

    @mark-bourke 的回答仅适用于单个分隔符。我修复了多个分隔符的 invalidateSeparators() 方法。我没有使用水平堆栈视图对其进行测试,但它适用于垂直堆栈视图:

      private func invalidateSeparators() {
        guard arrangedSubviews.count > 1 else {
          separators.forEach({$0.removeFromSuperview()})
          separators.removeAll()
          return
        }
    
        if separators.count > arrangedSubviews.count {
          separators.removeLast(separators.count - arrangedSubviews.count)
        } else if separators.count < arrangedSubviews.count {
          for _ in 0..<(arrangedSubviews.count - separators.count - 1) {
            separators.append(UIView())
          }
        }
    
        separators.forEach({$0.backgroundColor = self.separatorColor; self.addSubview($0)})
    
        for (index, subview) in arrangedSubviews.enumerated() where arrangedSubviews.count >= index + 2 {
          let nextSubview = arrangedSubviews[index + 1]
          let separator = separators[index]
    
          let origin: CGPoint
          let size: CGSize
    
          if axis == .horizontal {
            let originX = subview.frame.maxX + (nextSubview.frame.minX - subview.frame.maxX) / 2.0 + separatorInsets.left - separatorInsets.right
            origin = CGPoint(x: originX, y: separatorInsets.top)
            let height = frame.height - separatorInsets.bottom - separatorInsets.top
            size = CGSize(width: separatorWidth, height: height)
          } else {
            let originY = subview.frame.maxY + (nextSubview.frame.minY - subview.frame.maxY) / 2.0 + separatorInsets.top - separatorInsets.bottom
            origin = CGPoint(x: separatorInsets.left, y: originY)
            let width = frame.width - separatorInsets.left - separatorInsets.right
            size = CGSize(width: width, height: separatorWidth)
          }
    
          separator.frame = CGRect(origin: origin, size: size)
          separator.isHidden = nextSubview.isHidden
        }
      }
    

    【讨论】:

      【解决方案8】:

      一个很好的可可豆荚,可以很好地完成这项工作。 它使用 Swizzeling,编码良好。

      https://github.com/barisatamer/StackViewSeparator

      【讨论】:

        【解决方案9】:

        我赞成 @FrankByte.com 的回答,因为他帮助我找到了我的解决方案,这与 OP 想要做的非常相似:

        
        extension UIStackView {
            func addVerticalSeparators(color : UIColor, multiplier: CGFloat = 0.5) {
                var i = self.arrangedSubviews.count - 1
                while i > 0 {
                    let separator = createSeparator(color: color)
                    insertArrangedSubview(separator, at: i)
                    separator.heightAnchor.constraint(equalTo: self.heightAnchor, multiplier: multiplier).isActive = true
                    i -= 1
                }
            }
        
            private func createSeparator(color: UIColor) -> UIView {
                let separator = UIView()
                separator.widthAnchor.constraint(equalToConstant: 1).isActive = true
                separator.backgroundColor = color
                return separator
            }
        }
        

        【讨论】:

          【解决方案10】:

          您可以使用界面生成器来执行此操作。将父UIStackview的分布设置为Equal Centering,并在父UIStackView的开头和结尾添加两个宽度为0的UIView。然后在中间添加一个宽度为1的UIView,并设置UIView的背景颜色。

          【讨论】:

            【解决方案11】:

            @OwlOCR 解决方案的另一个版本,如果我们不希望在开头和结尾处有分隔符。

            extension UIStackView {
                func addHorizontalSeparators(color : UIColor) {
                    let separatorsToAdd = self.arrangedSubviews.count - 1
                    var insertAt = 1
                    for _ in 1...separatorsToAdd {
                        let separator = createSeparator(color: color)
                        insertArrangedSubview(separator, at: insertAt)
                        insertAt += 2
                        separator.heightAnchor.constraint(equalTo: self.heightAnchor, multiplier: 1).isActive = true
                    }
                }
            
                private func createSeparator(color : UIColor) -> UIView {
                    let separator = UIView()
                    separator.widthAnchor.constraint(equalToConstant: 1).isActive = true
                    separator.backgroundColor = color
                    return separator
                }
            }
            

            【讨论】:

              【解决方案12】:

              我的解决方案很简单。您可以在 UIStackView 中的轴属性上使用 switch 语句来检查当前正在使用哪个轴,而不是创建垂直轴和水平轴的方法。

              要添加分隔符,只需添加此扩展名并传递分隔符应放置的位置和颜色。

              extension UIStackView {
              
              func addSeparators(at positions: [Int], color: UIColor) {
                  for position in positions {
                      let separator = UIView()
                      separator.backgroundColor = color
                      
                      insertArrangedSubview(separator, at: position)
                      switch self.axis {
                      case .horizontal:
                          separator.widthAnchor.constraint(equalToConstant: 1).isActive = true
                          separator.heightAnchor.constraint(equalTo: self.heightAnchor, multiplier: 1).isActive = true
                      case .vertical:
                          separator.heightAnchor.constraint(equalToConstant: 1).isActive = true
                          separator.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: 1).isActive = true
                      @unknown default:
                          fatalError("Unknown UIStackView axis value.")
                      }
                  }
              }
              

              }

              示例用例:

              stackView.addSeparators(at: [2], color: .black)
              

              【讨论】:

                猜你喜欢
                • 2023-03-18
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多