Search code examples
iosswiftuilabel

How to add dots (ellipses) to LabelView as in the table of contents of a book


I have stackView which contains few labelViews in each of which two words are written. And I want them to be separated by an ellipsis across the entire width of labelView. As a result: one word close to the left, another - to the right, and dots between them. Note: the label can take up several lines if the words length is long.

enter image description here

EDIT

Here filling my stackView

for ingredient in ingredients {
    let textLabel = UILabel()
    textLabel.backgroundColor = UIColor.yellow // just for my needs
    textLabel.widthAnchor.constraint(equalToConstant: ingredientsStackView.frame.width).isActive = true
    textLabel.heightAnchor.constraint(equalToConstant: 20.0).isActive = true
    textLabel.text = ingredient.getName() + " " + String(ingredient.getAmount()) + " " + ingredient.getMeasure()
    textLabel.textAlignment = .left
    textLabel.numberOfLines = 0
    textLabel.lineBreakMode = .byWordWrapping
    ingredientsStackView.addArrangedSubview(textLabel)
}
ingredientsStackView.translatesAutoresizingMaskIntoConstraints = false

and that looks like this

enter image description here

But I want something like this

enter image description here

You can see dots between ingredientName and ingredientAmount.

I had an idea to implement this through CGFloat conversion here, but this question was closed.


Solution

  • One technique is to use size() or boundingRect(with:options:context:) to calculate the size, repeating that for more and more series of dots, until you reach the desired width.

    But that ignores a subtle (but IMHO, important) aspect, namely that the dots from all the rows should all line up perfectly. If they don’t line up, it can be surprisingly distracting.

    So, I’d be inclined to define a view that does that, performing a modulus calculation against some common ancestor view coordinate system. And, I’d personally just render the dots as UIBezierPath.

    For example:

    class EllipsesView: UIView {
        let spacing: CGFloat = 3
        let radius: CGFloat = 1.5
    
        var color: UIColor {
            UIColor { traitCollection in
                switch traitCollection.userInterfaceStyle {
                case .dark: return .white
                default:    return .black
                }
            }
        }
    
        let shapeLayer: CAShapeLayer = {
            let layer = CAShapeLayer()
            layer.strokeColor = UIColor.clear.cgColor
            return layer
        }()
    
        override init(frame: CGRect = .zero) {
            super.init(frame: frame)
            configure()
        }
    
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            configure()
        }
    
        override func layoutSubviews() {
            super.layoutSubviews()
    
            shapeLayer.fillColor = color.cgColor
    
            let point = convert(bounds.origin, to: window)
    
            let diff = radius * 3 + spacing
            let offset = diff - point.x.truncatingRemainder(dividingBy: diff)
    
            let rect = CGRect(x: bounds.minX + offset, y: bounds.maxY - radius * 2, width: bounds.width - offset, height: radius * 2)
    
            let path = UIBezierPath()
    
            var center = CGPoint(x: rect.minX + radius, y: rect.midY)
    
            while center.x + radius < rect.maxX {
                path.addArc(withCenter: center, radius: radius, startAngle: 0, endAngle: 2 * .pi, clockwise: true)
                center.x += diff
            }
    
            shapeLayer.path = path.cgPath
        }
    }
    
    private extension EllipsesView {
        func configure() {
            layer.addSublayer(shapeLayer)
        }
    }
    

    Then you can add your two labels, lining up the bottom of the ellipses view with the bottom baseline of the labels:

    let stringPairs = [("foo", "$1.37"), ("foobar", "$0.42"), ("foobarbaz", "$10.00"), ("foobarbazqux", "$100.00")]
    for stringPair in stringPairs {
        let container = UIView()
        container.translatesAutoresizingMaskIntoConstraints = false
    
        let leftLabel = UILabel()
        leftLabel.translatesAutoresizingMaskIntoConstraints = false
        leftLabel.text = stringPair.0
        leftLabel.setContentHuggingPriority(.required, for: .horizontal)
        container.addSubview(leftLabel)
    
        let ellipsesView = EllipsesView()
        ellipsesView.translatesAutoresizingMaskIntoConstraints = false
        container.addSubview(ellipsesView)
    
        let rightLabel = UILabel()
        rightLabel.translatesAutoresizingMaskIntoConstraints = false
        rightLabel.font = UIFont.monospacedDigitSystemFont(ofSize: rightLabel.font.pointSize, weight: .regular)
        rightLabel.text = stringPair.1
        rightLabel.setContentHuggingPriority(.required, for: .horizontal)
        container.addSubview(rightLabel)
    
        NSLayoutConstraint.activate([
            // horizontal constraints
    
            leftLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor),
            ellipsesView.leadingAnchor.constraint(equalTo: leftLabel.trailingAnchor, constant: 3),
            rightLabel.leadingAnchor.constraint(equalTo: ellipsesView.trailingAnchor, constant: 3),
            rightLabel.trailingAnchor.constraint(equalTo: container.trailingAnchor),
    
            // align last baseline of three subviews
    
            leftLabel.lastBaselineAnchor.constraint(equalTo: ellipsesView.bottomAnchor),
            leftLabel.lastBaselineAnchor.constraint(equalTo: rightLabel.lastBaselineAnchor),
    
            // vertical constraints to container
    
            leftLabel.topAnchor.constraint(greaterThanOrEqualTo: container.topAnchor),
            rightLabel.topAnchor.constraint(greaterThanOrEqualTo: container.topAnchor),
            ellipsesView.topAnchor.constraint(equalTo: container.topAnchor),
            leftLabel.bottomAnchor.constraint(equalTo: container.bottomAnchor),
        ])
    
        verticalStackView.addArrangedSubview(container)
    }
    

    That yields the ellipses, but they all line up perfectly, too:

    enter image description here