问题
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.
回答1:
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:
回答2:
Thanks for @koen. He gave me a link to the another question that also helped me. But I see that he deleted that comment maybe because Rob's answer. And I couldn't save it.
The idea was to add horizontal stacks of 3 views to the root vertical stack.
let stackView = UIStackView()
ingredientsStackView.addArrangedSubview(stackView)
stackView.leadingAnchor.constraint(equalTo: ingredientsStackView.leadingAnchor,
constant: 0.0).isActive = true
stackView.trailingAnchor.constraint(equalTo: ingredientsStackView.trailingAnchor,
constant: 0.0).isActive = true
Next create and set constraints for these views
let nameLabel = UILabel()
let ellipsesLabel = UILabel()
let amountAndMeasureLabel = UILabel()
for label in [nameLabel, ellipsisLabel, amountAndMeasureLabel] {
label.font = UIFont(name: "Montserrat-Medium", size: 14)
stackView.addArrangedSubview(label)
}
NSLayoutConstraint.activate([
nameLabel.leadingAnchor.constraint(equalTo: stackView.leadingAnchor, constant: 0.0),
nameLabel.trailingAnchor.constraint(equalTo: ellipsisLabel.leadingAnchor, constant: 0.0),
nameLabel.widthAnchor.constraint(equalToConstant: nameLabel.intrinsicContentSize.width),
amountAndMeasureLabel.leadingAnchor.constraint(equalTo: ellipsisLabel.trailingAnchor, constant: 0.0),
amountAndMeasureLabel.trailingAnchor.constraint(equalTo: stackView.trailingAnchor, constant: 0.0),
amountAndMeasureLabel.widthAnchor.constraint(equalToConstant: amountAndMeasureLabel.intrinsicContentSize.width)
])
After that DON'T FORGET to call stackView.layoutIfNeeded()
And then fill the middle view with dots until it is completely filled.
while ellipsisLabel.intrinsicContentSize.width <= ellipsisLabel.frame.width {
ellipsisLabel.text?.append(".")
}
Whole code
private func setIngredients(ingredients: [Ingredient], ingredientsStackView: UIStackView) {
for ingredient in ingredients {
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.alignment = .fill
stackView.distribution = .fill
ingredientsStackView.addArrangedSubview(stackView)
stackView.leadingAnchor.constraint(equalTo: ingredientsStackView.leadingAnchor,
constant: 0.0).isActive = true
stackView.trailingAnchor.constraint(equalTo: ingredientsStackView.trailingAnchor,
constant: 0.0).isActive = true
let nameLabel = UILabel()
nameLabel.text = ingredient.name
nameLabel.lineBreakMode = .byWordWrapping
nameLabel.contentMode = .left
nameLabel.numberOfLines = 0
let ellipsisLabel = UILabel()
ellipsisLabel.text = ""
let amountAndMeasureLabel = UILabel()
amountAndMeasureLabel.text = String(ingredient.amount) + " " + ingredient.measure
amountAndMeasureLabel.contentMode = .left
for label in [nameLabel, ellipsisLabel, amountAndMeasureLabel] {
label.font = UIFont(name: "Montserrat-Medium", size: 14)
stackView.addArrangedSubview(label)
}
NSLayoutConstraint.activate([
nameLabel.leadingAnchor.constraint(equalTo: stackView.leadingAnchor, constant: 0.0),
nameLabel.trailingAnchor.constraint(equalTo: ellipsisLabel.leadingAnchor, constant: 0.0),
nameLabel.widthAnchor.constraint(
equalToConstant: nameLabel.intrinsicContentSize.width),
amountAndMeasureLabel.leadingAnchor.constraint(equalTo: ellipsisLabel.trailingAnchor, constant: 0.0),
amountAndMeasureLabel.trailingAnchor.constraint(equalTo: stackView.trailingAnchor, constant: 0.0),
amountAndMeasureLabel.widthAnchor.constraint(
equalToConstant: amountAndMeasureLabel.intrinsicContentSize.width)
])
stackView.layoutIfNeeded()
while ellipsisLabel.intrinsicContentSize.width <= ellipsisLabel.frame.width {
ellipsisLabel.text?.append(".")
}
}
ingredientsStackView.translatesAutoresizingMaskIntoConstraints = false
}
BUT I did this before I saw @Rob answer. Thanks! Probably it's more difficult implementation compared to mine and you can choose which is better for you. I think his solution is more rational and cleaner so as an answer I will choose his post.
来源:https://stackoverflow.com/questions/62114198/how-to-add-dots-ellipses-to-labelview-as-in-the-table-of-contents-of-a-book