А заодно раз и навсегда решаем проблему автоматической калькуляции высоты ячеек.
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/
Добавить комментарий