Краткий обзор того, как заставить работать вложенные ячейки с изменяющимся размером

Идея этой статьи возникла, когда я столкнулся с проблемой в моей работе , мне дали задание реализовать вложенную версию self-sizing-cells: саморазмер UICollectionViewCell внутри саморазъема UITableViewCell.

На первый взгляд, это казалось довольно простой задачей, и я наивно думал, что все будет «просто работать» с самого начала, но, черт возьми, я ошибался.

С первых дней разработки iPhone UITableViews и UICollectionViews использовались почти в каждом приложении для отображения контента.

Обычно этот контент не статичен и не всегда имеет одинаковый размер, но с появлением динамического типа и разных размеров экрана важно правильно отображать контент на каждом устройстве.

Несколько лет назад, с выпуском iOS 8, Apple добавила возможность легко реализовать саморазмерные ячейки в UITableViews и UICollectionViews, не выполняя эти вычисления самостоятельно.

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

Если вы хотите продолжить, вы можете найти образец кода на GitHub.

Основы самостоятельного определения размера

В iOS 11 автоматическое изменение размера ячеек включено по умолчанию, но если нам все еще нужно поддерживать iOS 10, это очень легко заставить работать.

Нам просто нужно установить estimatedRowHeight равным приблизительному значению высоты и позволить UIKit выполнить расчет за нас. UICollectionViewFlowLayout не требует этого значения с iOS 10.

Система может заставить работать самоизмеряющиеся ячейки, автоматически вычисляя размер ячеек на основе ограничений, определенных в ее contentView.

Чтобы помочь автоматическому макету вычислить правильный размер, нам нужно убедиться, что у нас есть непрерывная цепочка ограничений для всех элементов пользовательского интерфейса от верхнего якоря до нижнего якоря. Мы должны убедиться, что у нас нет неоднозначного макета, иначе невозможно будет рассчитать размер.

Помимо простейшего случая

Когда мы убедимся, что у нас есть недвусмысленный макет, реализовать самоизмерение UITableView или UICollectionView довольно просто. Все становится немного сложнее, когда мы погружаемся в что-то более сложное, например, вкладываем одно в другое.

Чтобы объяснить, что я узнал об этом, мы рассмотрим несколько различных ситуаций, чтобы понять, как это работает.

Прежде чем мы начнем, несколько примечаний к образцу кода.

  • Мы пишем весь код UI программно, файлы Storyboards или xib не использовались.
  • Мы используем AutoLayout.

Квадратный вид

Чтобы упростить понимание, мы будем использовать квадратный вид фиксированного размера (80x80) в качестве единственного объекта внутри UICollectionViewCell.

Чтобы рассчитать правильный размер ячеек, автоматическая компоновка должна знать размер каждого отдельного элемента. В этом вложенном примере автоматический макет должен знать размер UICollectionViewCell для расчета размера UICollectionView (contentView).

Чтобы избежать сложностей, мы определили ширину и высоту 80 пунктов, чтобы облегчить автоматический макет при этом вычислении.

UICollectionView находится внутри UITableViewCell, поэтому автоматическому макету требуется его размер для вычисления размера UITableViewCell (contentView).

Если бы это было реализовано как автономный UICollectionView, определения размера элементов внутри ячейки было бы достаточно, но в этом вложенном примере мы получим что-то вроде этого:

Также мы видим это предупреждение в журнале:

Warning once only: Detected a case where constraints ambiguously suggest a height of zero for a table view cell’s content view. We’re considering the collapse unintentional and using standard height instead.

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

Это происходит потому, что вычисление размера UITableViewCell происходит до того, как UICollectionView contentSize было определено, поэтому он возвращает ноль.

Чтобы вычислить высоту ячейки с изменяющимся размером, UITableView вызывает systemLayoutSizeFitting(targetSize:horizontalFittingPriority:verticalFittingPriority:) для каждой ячейки. Этот метод вычисляет высоту на основе макета, определенного в ячейке.

Хороший способ отладить происходящее - переопределить этот метод и посмотреть, что возвращает вызов super. Когда мы это сделаем, мы увидим, что возвращаемый размер равен (375 x 44), а поскольку высота квадратного вида составляет 80 пунктов, содержимое обрезается.

Решения

Я искал в Интернете решение этой проблемы, и есть много предложений о том, как вернуть UICollectionView contentView из звонка systemLayoutSizeFitting.

Хотя это решение работает в большинстве случаев, как показано выше, в некоторых случаях оно не работает, что приводит к странно длинному UICollectionView.

Я обнаружил, что этот расчет не выполняется, потому что UICollectionView contentView иногда (пока) не изменяет размер до правильной ширины своего суперпредставления (UITableView) и использует значение по умолчанию UIKit, равное 320 пунктам.

Если мы запустим образец проекта и установим количество квадратов от 1 до 3, при расчете учитывается поле шириной 320 точек, и, поскольку это количество квадратов все еще умещается в этом поле, после прохода макета UITableView имеет правильную высоту. чтобы соответствовать этим квадратам.

К сожалению, в примере с четырьмя квадратами contentSize из UICollectionView также вычисляется для поля шириной 320 точек.

Это означает, что четыре квадрата необходимо разместить во втором ряду, и когда происходит переход макета, четыре квадрата будут размещены внутри одной строки (совпадающей с шириной UITableView), но UITableViewCell уже был рассчитан с неправильной высотой.

Решение для решения

Поигравшись с демонстрационным проектом, я обнаружил, что нам нужно принудительно выполнить дополнительный проход макета, чтобы UITableView мог вычислить правильную высоту ячейки, это также делается внутри метода systemLayoutSizeFitting:

Здесь происходит следующее: мы устанавливаем фрейм UITableView contentView на границы ячейки, а затем запускаем новый проход макета. Когда мы это сделаем, на следующем проходе автоматический макет сможет вычислить его размер, используя прямоугольник с правильной шириной.

Я пробовал это решение, используя разные макеты, особенно всегда сложную многострочную UILabel, и, похоже, оно последовательно решает проблему.

Я надеюсь, что есть более чистый способ сделать это, поэтому, если у вас есть какие-либо предложения, пришлите их мне.

Резюме

В этой статье мы увидели, как решить типичную проблему при реализации вложенных ячеек с самоизмерением.

В зависимости от вашего конкретного случая попробуйте отладить то, что возвращается в методе systemLayoutSizeFitting, и посмотрите на порядок проходов изменения размера и макета.

В Интернете есть хорошая информация по этой теме, я рекомендую Рено Линхарт в NSSpain talk.

Найдите пример кода, использованный в этой статье, на GitHub.

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