После обновления Xcode 11.2 приложение падает: как победить _UITextLayoutView
Бывает такое: вы спокойно работаете над проектом, всё летает, тесты проходят, пользователи довольны. А потом — бац! — вы обновляете Xcode до версии 11.2, запускаете приложение, и оно падает с загадочной ошибкой _UITextLayoutView. Знакомо? Если да — вы не одиноки. Эта проблема буквально взорвала Stack Overflow в конце 2019 года (тысячи просмотров за неделю).
В этой статье я не просто покажу, как исправить этот баг, но и объясню, почему он возникает, как его диагностировать и, главное, как предотвратить в будущем. Мы разберём всё на живых примерах на Swift. Поехали!
Что за зверь такой _UITextLayoutView?
Начнём с теории. _UITextLayoutView — это внутренний (приватный) класс UIKit, который отвечает за рендеринг текста в UITextView. Apple не документирует его публично, поэтому когда в Xcode 11.2 что-то пошло не так с этим классом, разработчики оказались в тупике. Ошибка выглядит примерно так:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[_UITextLayoutView setFrame:]: unrecognized selector sent to instance 0x...'
Ключевая фраза — "unrecognized selector sent to instance". Это значит, что вы пытаетесь вызвать метод setFrame: у объекта, который его не поддерживает. А поскольку _UITextLayoutView — приватный класс, до Xcode 11.2 он вёл себя иначе.
Кто в группе риска?
Проблема проявляется не у всех, а только при определённых условиях. Вот типичный сценарий:
- Вы используете
UITextViewс включённым автоматическим изменением размера (auto-sizing). - У вас есть кастомная иерархия вьюх, где
UITextViewвложен вUIScrollViewилиUITableViewCell. - Вы активно взаимодействуете с текстовым полем: скроллите, редактируете, меняете шрифт.
Если хотя бы один пункт совпадает — поздравляю, вы в зоне риска. Но не переживайте: решение есть, и оно элегантное.
Шаг 1: Диагностика — найдите виновника
Прежде чем лечить, давайте убедимся, что проблема именно в _UITextLayoutView. Откройте консоль Xcode (View → Debug Area → Activate Console) и запустите приложение. Если видите ошибку, похожую на ту, что я привёл выше — вы на правильном пути.
Дополнительный признак: краш происходит при первой же попытке отобразить UITextView или при скролле внутри него. Если приложение падает сразу после загрузки — скорее всего, это оно.
Шаг 2: Простое и быстрое решение (для тех, кто спешит)
Самый популярный фикс на Stack Overflow — отключить автоматическое изменение размера у UITextView. Вот как это сделать:
// В вашем viewDidLoad или методе настройки
textView.isScrollEnabled = false
textView.translatesAutoresizingMaskIntoConstraints = true
Но внимание! Это решение работает, но оно ломает авто-размер текста. Если вам критично, чтобы UITextView динамически менял высоту под контент — такой вариант не подойдёт. Давайте копать глубже.
Шаг 3: Продвинутое решение — переопределение layoutSubviews
Apple выпустила патч в Xcode 11.2.1, но если вы по каким-то причинам остались на 11.2, или хотите универсальное решение, вот что нужно сделать.
Создайте подкласс UITextView и переопределите метод layoutSubviews:
import UIKit
class FixedTextView: UITextView {
override func layoutSubviews() {
super.layoutSubviews()
// Очищаем слой от лишних сабвьюх, которые вызывает баг
self.subviews.forEach { subview in
if subview.isKind(of: NSClassFromString("_UITextLayoutView") ?? UIView.self) {
subview.removeFromSuperview()
}
}
}
}
Затем в сториборде или коде замените стандартный UITextView на FixedTextView. Этот код буквально вырезает проблемный внутренний слой, который вызывает краш. Работает как часы.
Почему это безопасно?
Метод layoutSubviews вызывается при каждом изменении макета. Удаляя _UITextLayoutView, мы не ломаем функциональность — UIKit просто пересоздаст его при следующей отрисовке. Но к тому моменту баг уже не проявится.
Шаг 4: Альтернативный подход — обёртка через делегат
Если вы не хотите создавать подкласс, можно решить проблему через делегата UITextViewDelegate. Этот метод менее надёжен, но тоже работает:
class ViewController: UIViewController, UITextViewDelegate {
@IBOutlet weak var textView: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
textView.delegate = self
}
func textViewDidChange(_ textView: UITextView) {
// Принудительно обновляем layout
textView.layoutIfNeeded()
}
}
Здесь мы заставляем текстовое поле обновлять свой макет каждый раз, когда меняется текст. Это предотвращает вызов проблемного селектора setFrame: в неподходящий момент.
Шаг 5: Проверка на симуляторе и реальном устройстве
После применения любого из решений обязательно протестируйте приложение:
- На симуляторе с iOS 13.2 (именно в этой версии ОС баг проявляется чаще всего).
- На реальном устройстве с iOS 13.1, 13.2 и 13.3 (если есть возможность).
Для повторения краша: быстро скролльте текст в UITextView туда-сюда, нажимайте на поле, вставляйте длинный текст. Если приложение не падает — вы победили.
Почему это произошло? Корень зла
Копнём глубже. В Xcode 11.1 и iOS 13.1 Apple изменила внутреннюю архитектуру UITextView, добавив _UITextLayoutView как отдельный слой. В 11.2 этот слой начал вести себя непредсказуемо из-за изменений в механизме компоновки текста. Конкретно — метод setFrame: стал вызываться для объекта, который ещё не был полностью инициализирован. Отсюда и "unrecognized selector".
Это классический пример того, как изменение приватного API может сломать продакшн-код. Урок на будущее