iOS - 在堆栈视图中以编程方式添加垂直线

18

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

期望的结果应该类似于这张图片:

stackview

目前我可以添加带有所需间距的标签;我可以添加水平线,但是我无法弄清楚如何在它们之间添加那些分隔符垂直线。

我想以类似于以下方式完成此操作:

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

有什么提示吗?

谢谢


创建一个宽度为2点的“UIView”(通过约束强制执行),颜色为纯黑色,并将两个视图添加到“UIStackView”中的正确位置。 - Robotic Cat
12个回答

45

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

  • 设置其背景颜色。
  • 将其宽度约束为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
}

结果:

result

完整的 Playground 代码(由 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))

这是一个很棒的技巧,可以帮助我实现我想要的设计; - Ivan Cantarino
我曾经遇到过类似的问题,这种技巧非常有效!一个优化方法可能是添加标签约束条件并跳过第一个。 - gprasant

32
你可以尝试以下步骤:
  1. 首先,创建一个UIView,并将UIStackView的约束应用于此UIView。
  2. 将此UIView的背景色设置为黑色(线条的颜色)。
  3. 现在,创建一个UIStackView并将其作为上述UIView的子视图添加进去。
  4. 添加UIStackView的约束,即将其绑定到父UIView的所有边缘上。
  5. 现在将UIStackView的背景色设置为透明色。
  6. 将UIStackView的间距设置为1或2(线的宽度)。
  7. 现在将3个标签添加到stackview中。
  8. 确保标签的背景色为白色,文本颜色为黑色。
通过以上步骤,你将实现所需场景。参考下面的图片。

enter image description here enter image description here


1
嗯...我从来没有想过那样做。这可能解决问题。我稍后会尝试一下,如果有效果的话,我会反馈的。谢谢。 - Ivan Cantarino
这正是我正在寻找的! - Roman Filippov
1
一个很好的解决方案:只需创建一个带有背景颜色的背景视图。 - evya
不错的想法... ;)) - duongvanthai
很棒的想法!干得好。 - Oz Shabat

19

这是一个简单的扩展程序,可在每一行之间添加分隔符(注意!是行而不是列,如所需,也可以轻松修改为列)。基本上与已接受的答案相同,但以可重复使用的格式提供。

使用时调用例如:

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

1
这是一个不错的东西。谢谢分享。 - Ivan Cantarino
工作得很好。谢谢! - Verticon
对于阅读此内容的任何人,如果您不想在顶部使用分隔符,请将while i >= 0更改为while i > 0。如果您不想在底部使用分隔符,请将var i = self.arrangedSubviews.count更改为var i = self.arrangedSubviews.count - 1 - Muhammed Gül

1
这是一个更灵活的UIStackView子类,支持任意添加排列的子视图,并适用于那些需要在其UIStackView和子视图上放置清晰背景的人,如下图所示。
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)
        }
    }
}

结果是什么?

SeparatorStackView on top of a UIVisualEffectView


1
在具有多个项目的垂直StackView上,这只会在第一个和第二个项目之间添加一个分隔符。 - Joseph
invalidateSeparators() 中创建分隔符数组的那一行会将同一个分隔符多次添加到数组中。 - Greg
它还会为第一个分隔符之外的任何分隔符计算错误的帧。正在修复中。 - Greg
我有多个项目中的stackView,为什么只显示第一个? - Digvijaysinh Gida

1
另一个版本的@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
    }
}

0
如果垂直线字符“|”符合您的要求,则可以将标签添加到堆栈视图中,以在所需位置添加分隔线。然后使用以下代码:
myStackView.distribution = .equalSpacing

您还可以在 Interface Builder 中更改 Stack View Distribution。


0

我的解决方案非常简单。不需要为垂直和水平轴创建方法,您可以在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)

0

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

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

enter image description here enter image description here


0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接