iOS - Add vertical line programatically inside a stack view

Viewed 25998

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

The desired finish will be something like this image:

stackview

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

12 Answers

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 e.g.

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?

SeparatorStackView on top of a UIVisualEffectView

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

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.

enter image description here enter image description here

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