Pull to refresh

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

Reading time 5 min
Views 7.6K
А заодно раз и навсегда решаем проблему автоматической калькуляции высоты ячеек.

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 и предоставляется исключительно в ознакомительных целях.

Спасибо за внимание.
Tags:
Hubs:
+7
Comments 3
Comments Comments 3

Articles