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.
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
But I want something like this
You can see dots between ingredientName
and ingredientAmount
.
I had an idea to implement this through CGFloat
conversion here, but this question was closed.
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: