iOS - Add vertical line programatically inside a stack view

后端 未结 10 864
悲哀的现实
悲哀的现实 2020-12-25 09:04

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 c

相关标签:
10条回答
  • 2020-12-25 09:39

    @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
        }
      }
    
    0 讨论(0)
  • 2020-12-25 09:42

    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
        }
    }
    
    0 讨论(0)
  • 2020-12-25 09:43

    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.

    0 讨论(0)
  • 2020-12-25 09:46

    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))
    
    0 讨论(0)
提交回复
热议问题