Динамические ячейки в статической таблице UITableView. С анимацией!

Пишу сейчас одну программу под iOS, и в ней мне понадобилось в окне настроек уметь прятать и показывать некоторые ячейки с дополнительными опциями. Настройки, понятное дело, сделаны на статической UITableView. Если просто спрятать ячейку, то она конечно спрячется, но занимаемое ею место продолжит занимать. Нужно что-то другое.

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

Проект на github.

Нюансы под катом.

Для динамического изменения высоты ячеек служит метод делегата tableView:heightForRowAtIndexPath:, это всем известно. Но вот что в документации не упомянуто, так это то, что изменение высоты ячеек анимируется — то, что доктор прописал. В вышеупомянутом методе на основании некоторого состояния возвращаем высоту интересующих нас ячеек (в самом простом случае — 0 или нормальную высоту ячейки), а когда нужно ячейки спрятать/показать — меняем состояние и инициируем обновление ячеек. Как инициируем? А вот так:

А теперь гвоздь программы, нюанс номер один. tableView:heightForRowAtIndexPath: получает на вход indexPath ячейки, для которой нужно определить высоту. Логично. Что менее логично, так это то, что вызов tableView.cellForRowAtIndexPath(indexPath) приводит к бесконечной рекурсии и, соотвественно, крэш тебе в бубен, мой дорогой программист. Ну ничего. Нормальные герои всегда идут в обход, проживем без tableView.cellForRowAtIndexPath:.

Первый вариант, который просится в голову, подойдет для не очень ленивых программистов: заложиться на конкретные значения indexPath.row. Но я вот, например, слишком ленив для такого решения и не имею никакого желания править баги, которые неизбежно возникнут в дальнейшем при изменении таблицы, после чего недостаточно ленивый программист должен будет все номера строк вручную менять на новые. Мы, истинно ленивые программисты, однозначно пойдем другим путем:

  1. все динамические ячейки оформляются как outlets;
  2. переопределятся tableView:cellForRowAtIndexPath:;
  3. для каждой ячейки динамической ячейки, возвращаемой из этого метода, сохраняется в сторонку ее индекс;
  4. в tableView:heightForRowAtIndexPath: для определения нужных мне ячеек используются сохраненные ранее индексы.

Итого, получается что-то в таком вот роде:

Но на этом нюансы не заканчиваются.

Обратите внимание, что динамические ячейки нигде не прячутся. Во-первых, тот же видимый эффект можно получить, установив для ячеек опцию Clip Subviews в Xcode (ну или программно). В этом случае высота равная нулю гарантирует, что ничего отображаться не будет. Во-вторых, представьте, что ячейка должна быть спрятана. Свойство hidden нужно ставить в true после анимации спрятывания, правильно? Ну а когда она закончится никто не знает. Нет в API никаких коллбэков по этому поводу; так что проще всего и не заморачиваться.

Также вы непременно заметите, что спрятывание стандартных ячеек (Basic, Detail, Subtitle) анимируется криво. Вначале текст сплющивается в ноль, и потом это безобразие ездит по экрану. Непорядок. Так что нужно использовать Custom ячейки. Благо, в этом ничего сложного нет.

Ну и наконец, Auto Layout. Вы же именно его будете использовать в Custom ячейках, вероятность 99.99%. Тогда учтите, что Auto Layout очень, очень не любит нулевые размеры. Если вы хотите получить эффект «сплющивания» содержимого, сплющивайте не в 0, а в 1 point например. Такого рода условие легко задать с помощью правила-неравенства. Ну а проще всего будет просто выровнять все контролы в ячейке по центру в вертикальной оси. В примере так и сделано, и для меток (labels) это выглядит естественно.

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

Добавить комментарий