简体   繁体   中英

iOS - Add vertical line programatically inside a stack view

I'm trying to add vertical lines, between labels inside a stack view all programatically.

The desired finish will be something like this image:

堆栈视图

I can add the labels, all with the desired spacing; I can add horizontal lines but I can't figure out how to add those separator vertical lines in-between.

I'd like to do it something like this:

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

Any hint?

Thanks

You can't use the same view in two places, so you'll need to create two separate vertical line views. You need to configure each vertical line view like this:

  • Set its background color.
  • Constrain its width to 1 (so you get a line, not a rectangle).
  • Constrain its height (so it doesn't get stretched to the full height of the stack view).

So add the labels one at a time to the stack view, and do something like this before adding each label to the stack view:

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
}

Note that you do not want the vertical lines to be the same width as the labels, so you must not set the distribution property of the stack view to fillEqually . Instead, if you want all the labels to have equal width, you must create width constraints between the labels yourself. For example, after adding each new label, do this:

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

Result:

结果

Full playground code (updated to Swift 4.1 by Federico Zanetello):

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

You can try the following.

  1. First of all take a UIView and apply the same constraints of UIStackView to this UIView.
  2. Make the Background color of this UIView to Black (The color of the lines)
  3. Now take a UIStackView and add it as a child of above UIView.
  4. Add constraints of the UIStackView ie bind it to all the edges of parent UIView.
  5. Now make the bakckground color of UIStackView to Clear Color.
  6. Set the spacing of UIStackView to 1 or 2 (the width of lines)
  7. Now add the 3 labels into stackview.
  8. Make sure the labels have background color to White Color and Text Color to Black Color.

Thus you'll achieve the required scene. See these pictures for reference.

在此处输入图片说明 在此处输入图片说明

Here's a simple extension for adding separators between each row (NOTE! Rows, not columns as asked! Simple to modify for that case as well) . Basically same as accepted answer, but in a reusable format.

Use by calling eg

yourStackViewObjectInstance.addHorizontalSeparators(color : .black)

Extension:

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

Here's a more flexible UIStackView subclass that supports to arbitrary addition of arranged subviews and is suitable for those that need a clear background on their UIStackView and subviews to put on top of a UIVisualEffectView , like the picture below.

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

The result?

位于 UIVisualEffectView 之上的 SeparatorStackView

If the vertical line character, "|", works for the look you want, then you can add labels into the Stack View where you want separator lines. Then use:

myStackView.distribution = .equalSpacing

You can also change the Stack View Distribution in Interface Builder.

@GOR answer extension only for vertical line and only in centers

Stackview settings: set width contraints of each subview and parent stackview should be fill

Here's a simple extension for adding vertical separators between each row.

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
} 

@mark-bourke's answer only worked for a single separator. I fixed the invalidateSeparators() method for multiple separators. I haven't tested it with horizontal stack views, but it works for vertical ones:

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

a nice cocoa-pod that do the job pretty well. It uses Swizzeling, well coded.

https://github.com/barisatamer/StackViewSeparator

在此处输入图片说明

I've upvoted @FrankByte.com's answer because he helped me get to my solution, which is pretty similar to what the OP wanted to do:


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

You can use interface builder to do this. Set the distribution of the parent UIStackview to Equal Centering and add two UIView with width 0 in the beginnging and ending of the parent UIStackView. Then add a UIView with width 1 in the middle and set the background color of the UIView.

在此处输入图片说明 在此处输入图片说明

Another version of @OwlOCR's solution, if we don't want separators at the beginning and end.

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

My solution is very simple. Instead of creating method for vertical and horizontal axis, you can use switch statement on axis property inside UIStackView to check which axis is currently being used.

To add separators simply just add this extension and pass positions of where separators should be placed and color.

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

}

Example use case:

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM