Design cell prototypes in one XIB with UITableView

  • Tutorial
And at the same time, once and for all, we solve the problem of automatic calculation of cell height.

Disclaimer:
This method probably does not provide the best performance, it may have some pitfalls, cause dizziness, nausea and the devil, please do not read for the elderly and pregnant children.

Creation Overcoming difficulties is probably the best motivation for a programmer. It's no secret, Xcode contains a lot of flaws, opaque solutions and bugs. Today I will try to find a solution to one of them



. IOS5 introduced a wonderful feature - Storyboard, as well as the ability to create cell prototypes right inside the table being created (which, however, will be compiled into separate NIBs). However, they decided not to introduce the new functionality into regular XIBs.

I was a little puzzled that in fresh Xcode, you can still create a UITableViewController in which there will immediately be a table, and even a cell prototype. However, when compiling Xcode will give an error saying that you can’t do this.

So we got to the question “why” :

Suppose there are two large storyboards. Why two? Because if you shove a mountain of views, buttons and tablets into one, then even the brand new and frisky (though a year ago) MacBook Pro Retina 13 "turns into a pumpkin.

So, there are two storyboards, and let's say both of them have to open one and the same the same controller under different circumstances.

An inquisitive reader will notice that in iOS9 they introduced links to an external storyboard, but what if the project requires support for iOS8, or even 7? Unfortunately, the guys from Cupertino do not like to add backward compatibility.

A possible solution would be to create a ViewController without a View and an external Xib with the same class name so that it is automatically loaded with the loadView method. Or we explicitly set the nibName property:


It seems that there was an explicit field for this before, but in Xcode 7 I did not find it.

And here we are faced with the problem that the prototypes of the cells for the table will have to be put in external Xibs, one at a time in the file, and explicitly register them in the code. After using Storyboard I don’t feel like doing it at all.

And here is what you can do (the code will be in Objective-C, since black magic is used later):

Create an .h file with the following contents:
@interface UITableView (XibCells)
@property (nonatomic, strong) IBOutletCollection(UITableViewCell) NSArray* cellPrototypes;
@end

This will allow you to drop cells into the Interface Builder file (though not inside the table, but next to it), and connect them to IBOutletCollection cellPrototypes



Now you need to somehow palm off the loaded cells to the table so that it loads them as needed.
To do this, in the .m file, create a UINib descendant with preloaded data and override the instantiateWithOwner: options method :
@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

When the PrepopulatedNib object is initialized, the transferred array is archived in NSData using NSKeyedArchiver.
Next, the UITableView calls the instantiateWithOwner: nil options: nil method, and we unzip the array back, thus creating a copy of the objects. The cells obtained in this way are 100% identical, as they have just been unzipped from NIB and comply with the NSCoding protocol.

Final touch: make the table bind the passed cells and PrepopulatedNib:
@implementation UITableView (XibCells)
- (void)setCellPrototypes:(NSArray*)cellPrototypes {
  for (UITableViewCell* cell in cellPrototypes) {
      [self registerNib:[PrepopulatedNib nibWithObjects:@[cell]] forCellReuseIdentifier:cell.reuseIdentifier];
  }
}
@end

Now the table can work as if it were loaded from the Storyboard. Here you can get a little confused and at the first call return the original cell objects transferred in the array so that the resources are not wasted, but they will be useful to us later:

So, about the automatic calculation of cell height
In iOS8, finally introduced out-of-the-box calculation cell heights when using Layout Constraints (although it was buggy strongly). In iOS9, this feature was polished and Stack Views added. Again, there is no question of any backward compatibility.

I propose a convenient solution to this problem using code for loading cells from one XIB.

One way to calculate the height is to store one invisible instance of UITableViewCell with installed constraints. To do this in the proceduretableView: heightForRowAtIndexPath: in such an instance, the item / text of the future cell is set, and after calling the [cell layoutIfNeeded] method , cell.frame.size.height is returned .

We will use our preloaded cells for this method. To do this, we will store the cells in the NSDictionary associated with the table. To do this, add instructions to the .m file
#import 

In the setCellPrototypes: method, create an NSDictionary with cells, where the key is reuseIdentifier:
@implementation UITableView (XibCells)
static char cellPrototypesKey;
- (void)setCellPrototypes:(NSArray *)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

The cellPrototypeWithIdentifier: declaration will need to be pushed to the .h file so that it can be used in the code.
@interface UITableView (XibCells)
@property (nonatomic, strong) IBOutletCollection(UITableViewCell) NSArray* cellPrototypes;
- (UITableViewCell*)cellPrototypeWithIdentifier:(NSString*)reuseIdentifier;
@end

Now in the datasource code you can use prototypes to calculate the height:
- (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;
}


The code on purpose does not constitute an all-in-one solution, as it is a Proof of concept and is provided for informational purposes only.

Thanks for attention.

Also popular now: