У меня есть UITextView, содержащий NSAttributedString. Я хочу изменить размер текстового представления так, чтобы при фиксированной ширине отображалась вся строка без прокрутки.
NSAttributedString имеет метод, который позволяет вычислить его ограничивающий прямоугольник для заданного размера.
let computedSize = attributedString.boundingRect(with: CGSize(width: 200, height: CGFloat.greatestFiniteMagnitude),
options: .usesLineFragmentOrigin,
context: nil)
Но, к сожалению, это не работает, так как всегда возвращает высоту одной строки.





После нескольких попыток я выяснил, что NSAttributedString, который я устанавливал, имеет byTruncatingTail как значение lineBreakMode для NSParagraphStyle (это значение по умолчанию, которое мы используем в нашем приложении).
Чтобы добиться желаемого поведения, мне нужно изменить его на byWordWrapping или byCharWrapping.
let paragraphStyle = NSMutableParagraphStyle()
// When setting "byTruncatingTail" it returns a single line height
// paragraphStyle.lineBreakMode = .byTruncatingTail
paragraphStyle.lineBreakMode = .byWordWrapping
let stringAttributes: [NSAttributedString.Key: Any] = [.font: UIFont(name: "Avenir-Book", size: 16.0)!,
.paragraphStyle: paragraphStyle]
let attributedString = NSAttributedString(string: string,
attributes: stringAttributes)
let computedSize = attributedString.boundingRect(with: CGSize(width: 200, height: CGFloat.greatestFiniteMagnitude),
options: .usesLineFragmentOrigin,
context: nil)
computedSize.height
Обратите внимание, что при установке атрибутированной строки со значением byTruncatingTail на UILabel (где значение numberOfLines равно 0), строка «автоматически» становится многострочной, чего не происходит при вычислении boundingRect.
Есть и другие факторы, которые следует учитывать при вычислении высоты NSAttributedString для использования внутри UITextView (каждый из них может привести к тому, что строка не будет полностью содержаться в текстовом представлении):
1. Пересчитайте высоту при изменении границ.
Поскольку высота основана на границах, ее следует пересчитывать при изменении границ. Это может быть достигнуто с помощью KVO на пути к клавишам bounds, что делает макет недействительным при этом изменении.
observe(\.bounds) { (_, _) in
invalidateIntrinsicContentSize()
layoutIfNeeded()
}
В моем случае я аннулирую intrinsicContentSize моего пользовательского UITextView, поскольку я определяю его размер на основе вычисленной высоты строки.
2. Используйте ширину NSTextContainer.
Используйте textContainer.width (вместо bounds.width) в качестве фиксированной ширины для использования для вызова метода boundingRect, поскольку он учитывает любое значение textContainerInset (хотя значения left и right по умолчанию равны 0)
3. Добавьте вертикальные значения textContainerInsets к высоте строки.
После вычисления высоты NSAttributedString мы должны добавить textContainerInsets.top и textContainerInsets.bottom, чтобы вычислить правильную высоту UITextField (их значения по умолчанию - 8,0 ...)
override var intrinsicContentSize: CGSize {
let computedHeight = attributedText.boundingHeight(forFixedWidth: textContainer.size.width)
return CGSize(width: bounds.width,
height: computedHeight + textContainerInset.top + textContainerInset.bottom)
}
4. Удалите строкуFragmentPadding.
Установите 0 в качестве значения lineFragmentPadding или, если хотите, не забудьте удалить его значение из «фиксированной ширины» перед вычислением высоты NSAttributedString.
textView.textContainer.lineFragmentPadding = 0
5. Примените ceil к расчетной высоте.
Значение высоты, возвращаемое boundingRect, может быть дробным, если мы используем его как есть, это потенциально может привести к тому, что последняя строка не будет отображаться. Передайте его функции ceil, чтобы получить верхнее целое значение, чтобы избежать округления в меньшую сторону.
Возможный способ сделать это - создать подкласс UITextView, чтобы сообщать вам, когда его contentSize изменился (~ размер текста).
class MyExpandableTextView: UITextView {
var onDidChangeContentSize: ((CGSize) -> Void)?
override var contentSize: CGSize {
didSet {
onDidChangeContentSize?(contentSize)
}
}
}
В «Родительском представлении»:
@IBOulet var expandableTextView: MyExpandableTextView! //Do not forget to set the class in the Xib/Storyboard
// or
var expandableTextView = MyExpandableTextView()
И применив эффект:
expandableTextView. onDidChangeContentSize = { [weak self] newSize in
// if you have a NSLayoutConstraint on the height:
// self?.myExpandableTextViewHeightConstraint.constant = newSize.height
// else if you play with "frames"
// self?.expandableTextView.frame.height = newSize.height
}