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

点点圈 提交于 2020-06-26 12:25:40

问题


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

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!