Размер UITextView для соответствия многострочному NSAttributedString

У меня есть UITextView, содержащий NSAttributedString. Я хочу изменить размер текстового представления так, чтобы при фиксированной ширине отображалась вся строка без прокрутки.

NSAttributedString имеет метод, который позволяет вычислить его ограничивающий прямоугольник для заданного размера.

let computedSize = attributedString.boundingRect(with: CGSize(width: 200, height: CGFloat.greatestFiniteMagnitude),
                                                   options: .usesLineFragmentOrigin,
                                                   context: nil)

Но, к сожалению, это не работает, так как всегда возвращает высоту одной строки.

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
0
27
2

Ответы 2

После нескольких попыток я выяснил, что 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
}

Другие вопросы по теме