Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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:

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

like image 723
Ivan Cantarino Avatar asked May 21 '17 15:05

Ivan Cantarino


3 Answers

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:

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))
like image 187
rob mayoff Avatar answered Oct 20 '22 18:10

rob mayoff


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

enter image description here enter image description here

like image 32
Himanshu Garg Avatar answered Oct 20 '22 17:10

Himanshu Garg


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
    }
}
like image 17
OwlOCR Avatar answered Oct 20 '22 18:10

OwlOCR