📜 ⬆️ ⬇️

Design prototypes of cells in the same XIB with UITableView

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

Disclaimer:
Probably, this method does not provide the best performance, may have a certain amount of pitfalls, cause dizziness, nausea and the devil, please do not read the old and pregnant children.

Creating Overcoming difficulties is probably the best motivation for a programmer. It's not a secret, Xcode contains a lot of flaws, non-transparent solutions and bugs. Today I will try to find a solution to one of them.
')


In iOS5, they introduced a remarkable feature - Storyboard, as well as the ability to create prototypes of cells right inside the created table (which, nevertheless, are compiled into separate NIBs). However, they decided not to implement the new functionality in the usual XIBs.

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

So we approached the question "why?"

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

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

An inquisitive reader will notice that iOS9 has entered links to an external storyboard, but what if the project requires iOS8 support, or even 7? Unfortunately, the guys from Cupertino don't 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 by the loadView method. Or explicitly set the nibName property:


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

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

And here's how to do it (the code will be in Objective-C, since black magic is used further):

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

This will allow cells to be thrown into the Interface Builder file (but not inside the table, but nearby), and connect them to the IBOutletCollection cellPrototypes



Now you need to somehow slip the loaded cells into 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 to NSData using NSKeyedArchiver.
Next, 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, since they have just been unarchived from NIB-a and comply with the NSCoding protocol.

The final touch: make the table link the transferred 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 was loaded from the Storyboard. Here you can get a little confused and when you first call to return the original objects of the cells transferred in the array, so that the resources are not in vain, but they will come in handy later:

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

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

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

Let's 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 the .m file instruction
 #import <objc/runtime.h> 

In the setCellPrototypes method: create an NSDictionary with cells, where the key is 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 

The declaration of cellPrototypeWithIdentifier: will need to be moved 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, since it is a Proof of concept and is provided for informational purposes only.

Thanks for attention.

Source: https://habr.com/ru/post/271701/


All Articles