Дизайним прототипы ячеек в одном XIB-е с UITableView

от автора

А заодно раз и навсегда решаем проблему автоматической калькуляции высоты ячеек.

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

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


В iOS5 ввели замечательную фичу — Storyboard, а также возможность создавать прототипы ячеек прямо внутри создаваемой таблицы (которые, тем не менее, компилятся в отдельные NIB-ы). Однако новый функционал решили не внедрять в обычные XIB-ы.
Меня немного озадачило, что в свежем Xcode все-таки можно создать UITableViewController, в котором сразу будет таблица, и даже прототип ячейки. Однако при компиляции Xcode выдаст ошибку, что так делать мол нельзя.

Так мы подошли к вопросу «а зачем»:
Допустим, есть два больших сториборда. Почему два? Потому что, если пихать гору вьюшек, кнопочек и табличек в один, то даже новенький и резвый (правда год назад) MacBook Pro Retina 13" превращается в тыкву.
Итак, есть два сториборда, и, допустим, оба они должны открыть один и тот же контроллер при разном стечении обстоятельств.
Пытливый читатель заметит, что в iOS9 ввели ссылки на внешний storyboard, но что если проект требует поддержки iOS8, а то и 7? К сожалению, обратной совместимости ребята из Купертино добавлять не любят.

Возможным решением будет создать ViewController без View и внешний Xib с таким же именем класса, чтобы он автоматически подгрузился методом loadView. Или явно устанавливаем свойство nibName:


Кажется, раньше для этого было явное поле, но в Xcode 7 я его не нашел.

И тут мы сталкиваемся с проблемой, что прототипы ячеек для таблицы придется класть во внешние Xib-ы, причем по одному в файл, и явно регистрировать их в коде. После использования Storyboard совсем не хочется так делать.

И вот как можно поступить (код будет на Objective C, так как далее используется чорная магия):

Создаем .h файл со следующим содержанием:

@interface UITableView (XibCells) @property (nonatomic, strong) IBOutletCollection(UITableViewCell) NSArray* cellPrototypes; @end 

Это позволит закинуть в файл Interface Builder ячейки (правда не внутрь таблицы, а рядом), и подключить их к IBOutletCollection cellPrototypes

Теперь надо как-то подсунуть загруженные ячейки таблице, чтобы она их подгружала по мере необходимости.
Для этого в .m файле создадим наследника UINib с предварительно загруженными данными и переопределим метод instantiateWithOwner:options:

@interface PrepopulatedNib: UINib @property (nonatomic, strong) NSData* nibData; @end  @implementation PrepopulatedNib + (instancetype)nibWithObjects:(NSArray*)objects {   PrepopulatedNib* nib = [[self alloc] init];   nib.nibData = [NSKeyedArchiver archivedDataWithRootObject:objects];   return nib; } - (NSArray *)instantiateWithOwner:(id)ownerOrNil options:(NSDictionary *)optionsOrNil {   return [NSKeyedUnarchiver unarchiveObjectWithData:_nibData]; } @end 

При инициализации объекта PrepopulatedNib переданный массив архивируется в NSData с помощью NSKeyedArchiver.
Далее UITableView вызывает метод instantiateWithOwner:nil options:nil, и мы разархивируем массив обратно, создавая таким образом копию объектов. Ячейки, полученные таким образом 100% идентичны, так как только что были разархивированы из NIB-a и соответствуют протоколу NSCoding.

Последний штрих: заставить таблицу связать переданные ячейки и PrepopulatedNib:

@implementation UITableView (XibCells) - (void)setCellPrototypes:(NSArray*)cellPrototypes {   for (UITableViewCell* cell in cellPrototypes) {       [self registerNib:[PrepopulatedNib nibWithObjects:@[cell]] forCellReuseIdentifier:cell.reuseIdentifier];   } } @end 

Теперь таблица может работать, как будто ее загрузили из Storyboard. Тут можно немного заморочиться и при первом вызове возвращать оригинальные объекты ячеек, переданные в массиве, чтобы ресурсы не пропадали даром, но они нам пригодятся позже:

Итак, об автоматической калькуляции высоты ячеек
В iOS8 наконец-то ввели out-of-the-box вычисление высоты ячеек при использовании Layout Constraints (хотя и глючило оно сильно). В iOS9 эту функцию отполировали и добавили Stack Views. Опять же ни о какой обратной совместимости речи нет.

Предлагаю удобное решение этой задачи с использованием кода для подгрузки ячеек из одного XIB-а

Одним из способов вычисления высоты является хранение по одному невидимому экземпляру UITableViewCell с установленными constraint-ами. Для этого в процедуре tableView:heightForRowAtIndexPath: в таком экземпляре устанавливается item/text будущей ячейки, и, после вызова метода [cell layoutIfNeeded], возвращается cell.frame.size.height.

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

#import <objc/runtime.h>

В методе setCellPrototypes: создадим NSDictionary с ячейками, где ключ — reuseIdentifier:

@implementation UITableView (XibCells) static char cellPrototypesKey; - (void)setCellPrototypes:(NSArray<UITableViewCell *> *)cellPrototypes {   NSMutableDictionary* dict = [NSMutableDictionary dictionaryWithCapacity:cellPrototypes.count];   for (UITableViewCell* cell in cellPrototypes) {       [self registerNib:[PrepopulatedNib nibWithObjects:@[cell]] forCellReuseIdentifier:cell.reuseIdentifier];       dict[cell.reuseIdentifier] = cell;   }   objc_setAssociatedObject(self, &cellPrototypesKey, cellPrototypes, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (NSArray*)cellPrototypes { return nil; } //Чтобы не было warning-a - (UITableViewCell *)cellPrototypeWithIdentifier:(NSString *)reuseIdentifier {   NSDictionary* dict = (NSDictionary*)objc_getAssociatedObject(self, &cellPrototypesKey);   return dict[reuseIdentifier]; } @end 

Объявление cellPrototypeWithIdentifier: нужно будет вынести в .h файл, чтобы его можно было использовать в коде.

@interface UITableView (XibCells) @property (nonatomic, strong) IBOutletCollection(UITableViewCell) NSArray* cellPrototypes; - (UITableViewCell*)cellPrototypeWithIdentifier:(NSString*)reuseIdentifier; @end 

Теперь в коде datasource можно использовать прототипы для вычисления высоты:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {     id cellItem = _items[indexPath.section][indexPath.row];     MyTableViewCell* cell = [tableView cellPrototypeWithIdentifier:@"Cell"];     cell.item = cellItem;     [cell layoutIfNeeded];     return cell.frame.size.height; } 

Код нарочно не представляет собой all-in-one решения, так как является Proof of concept и предоставляется исключительно в ознакомительных целях.
Спасибо за внимание.

ссылка на оригинал статьи http://habrahabr.ru/post/271701/


Комментарии

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *