start

Let’s go from Cell.

For a long time, I setModel directly to Cell, and then override setModel: make various judgments inside to set Cell contents, like this code snippet:

@interface Model // ... / /... @end @interface Cell // ... / /... @property (nonatomic, strong, nullable) Model *model; @end @implementation Cell // ... / /... - (void)setModel:(Model *)model { // ... _stateLabel.text = model.isLiked ? @" like ":" like "; } @endCopy the code

Like the Model here, are generally from the network request or other ways to get the original data, directly brought to make it difficult, according to the business to set up the display content, simple business is good, if it is complex business here may become a pile of judgment.

Level 1 DataSource

If you write too much of the above pattern, you will find some drawbacks: reusing cells requires a specific Model, and you will worry about affecting the old logic if you try to add new business to setModel:.

I pondered how to solve these problems. From the Cell’s point of view, all it needs is some display data and some interaction events to pass out. The simplest way to display data is to provide whatever is needed, as shown in the following code snippet:

@protocol DataSource // ... / /... @property (nonatomic, copy, readonly) NSAttributedString *likingStateText; @end @protocol Delegate; @interface Cell @property (nonatomic, weak, nullable) id<DataSource> dataSource; @property (nonatomic, weak, nullable) id<Delegate> delegate; @end@implementation - (void)setDataSource:(id<DataSource>) DataSource {// / /... _stateLabel.attributedText = dataSource.likingStateText; } @endCopy the code

Remove the Model binding and add the DataSource to declare only the data required by the Cell. For example, if the Cell needs a rich text likingStateText, add an attribute to the DataSource.

In this way, the Cell only knows what it needs, so the business processing is lost in the Cell and moved to the DataSource implementation.

Level 2 Item mode

Currently, any class that implements a DataSource can be set to the Cell, so there can be multiple DataSource implementations for different business logic and the Cell can be reused to the maximum extent.

The following code snippet implements the protocol DataSource using the raw data and business logic:

@interface Model // Raw data //... / /... @end @protocol DataSource // ... / /... @property (nonatomic, copy, readonly) NSAttributedString *likingStateText; DataSource @interface Item<DataSource> - (instancetype)initWithModel:(Model *) Model; @end @implementation Item - (instancetype)initWithModel:(Model *)model { // ... //... _likingStateText = [NSAttributedString alloc initWithString: model isLiked? @ "have thumb up" : @ "thumb up"]. } @endCopy the code

Data flow is shown as follows:

As mentioned above, Item implements the protocol DataSource and passes in the original data Model for business logic processing during initialization.

Through different implementation of DataSource, business processing can be distributed to different items. To the Cell, it always sees a DataSource.

Level 3 Item Upgrade – Registration information

In my opinion, the emergence of Item is inevitable, and its capabilities are not limited to this.

As we all know, Cell needs to be registered in CollectionView before it is used, and then size calculation is involved.

I first take the registration Cell as an example. The registration Cell is nothing more than using Identifier, Class or Nib to register, and Item is the best understanding of the Cell. It is better to make the registration information into the attribute of Item, and then find an opportunity to read the information for registration. The extension code for Item looks like this:

@interface Item<DataSource> // ... / /... @property (nonatomic, readonly) Class cellClass; // Cell class; @property (nonatomic, readonly, nullable) UINib *cellNib; // if it is xib, return nib; @end @implementation Item - (Class)cellClass { return DemoCollectionViewCell.class; } - (UINib *_Nullable)cellNib { return nil; } @endCopy the code

You may notice that the Identifier attribute is missing, but it is not required. The class name itself is unique, and NSStringFromClass(cellClass) is already the best Identifier.

After solving the problem of registration information, we temporarily put aside the timing of registration and take a look at the calculation of size. Also, since Item knows the Cell well, it must know how to calculate the appropriate size, so we extend Item again as follows:

@interface Item<DataSource>
// ...
// ...
- (CGSize)layoutSizeThatFits:(CGSize)size atIndexPath:(nullable NSIndexPath *)indexPath collectionViewScrollDirection:(UICollectionViewScrollDirection)scrollDirection;
@end
Copy the code

SizeThatFits: : this method is similar to uiView.sizethatFits: : : this method returns the most appropriate size for Cell layout.

Level 4 Item Upgrade – Dynamic

Prior to this, business processing was done during the Item initialization build, and the processing of data was somewhat less dynamic. For example, if you want to modify an attribute of Item and update the Cell in time, what should you do?

If you want to update Item in a timely manner, you need to bind a Cell to it. When the Item attribute is modified, call the cell-related method to refresh the display. The following is the extension code for Item:

@interface Cell // ... / /... - (void)reloadLikingStatus; @end @interface Item<DataSource> // ... / /... - (void)bindCell:(__kindof UICollectionViewCell *)cell atIndexPath:(NSIndexPath *)indexPath; - (void)unbindCell:(__kindof UICollectionViewCell *)cell atIndexPath:(NSIndexPath *)indexPath; @property (nonatomic, getter=isLiked) BOOL liked; @end@implementation Item {__weak DemoCollectionViewCell *_cell; } / /... / /... - (void)bindCell:(Cell *)cell atIndexPath:(NSIndexPath *)indexPath { cell.dataSource = self; _cell = cell; // bind} - (void)unbindCell:(__kindof UICollectionViewCell *)cell atIndexPath:(NSIndexPath *)indexPath {_cell = nil; } - (void)setLiked:(BOOL)isLiked {//... / /... if ( _cell ! = nil ) [_cell reloadLikingStatus]; // Refresh the cell to display} @endCopy the code

As mentioned above, when updating an attribute of Item, refresh the Cell in time by rewriting the corresponding set method.

When adding dynamism through the above scheme, the size is likely to change as the data content changes. In practical application scenarios, in order to avoid repeated calculation of size, we generally cache the size of each Item. Therefore, we need to provide a mechanism to refresh the cache and identify which Item needs to refresh its size. Since the action Item to modify an attribute is aware (we can override the attribute’s set method), we let the Item identify itself. The extension code is as follows:

@interface Item<DataSource> // ... / /... @property (nonatomic, readonly) BOOL needsLayout; - (void)setNeedsLayout; @end @implementation Item - (instancetype)initWith... {/ /... / /... [self setNeedsLayout]; Void setNeedsLayout {_needsLayout = YES; } - (CGSize)layoutSizeThatFits:(CGSize)size atIndexPath:(nullable NSIndexPath *)indexPath collectionViewScrollDirection:(UICollectionViewScrollDirection)scrollDirection { // ... / /... // When this method is called, it means that the latest size is being calculated; // Since it is the latest size, you can reset _needsLayout to NO. } - (void)setLiked:(BOOL)isLiked { // ... / /... // Where the simulated content is changed, the size also needs to be recalculated; // call 'setNeedsLayout' to indicate that size needs to be recalculated; [self setNeedsLayout]; } @endCopy the code

The above processing mechanism is similar to uiView.setNeedslayout or calayer.setNeedslayout, which is equivalent to marking up the update operation until the next time the loop is run.

Level 5 Item upgrade – Event processing

Now let’s return to processing the Cell event.

In the old way, you end up setting the Delegate for the Cell in the VC. Here are some code snippets of the old schema:

/ / this is the old model, used for subsequent analysis / / -- -- -- -- -- -- -- -- -- -- -- -- -- -- cell -- -- -- -- -- -- -- -- -- -- -- -- -- -- @ protocol Delegate / /... / /... - (void)likingItemWasTappedOnCell:(Cell *)cell; // @end @interface Cell // ... / /... @property (nonatomic, weak, nullable) id<Delegate> delegate; @end // -------------- vc -------------- #import "Cell.h" @interface ViewController<Delegate> @end @implementation ViewController // ... / /... - (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath { // ... / /... // Set the delegate if ([cell isKindOfClass: cell.class]) [(cell *)cell setDelegate:self]; } // Cell - Delegate methods - (void)likingItemWasTappedOnCell:(Cell *)cell { // ... / /... } @endCopy the code

In this mode, VC and Cell will interact directly, from importing header files, to registering, to setting up the model and proxy and implementing proxy methods, as well as processing and interaction within proxy methods for business purposes.

The above only demonstrates a case of a Cell. In actual development, as the business complexity increases, the number of cells will increase, and maintenance in VC will become more and more difficult, and modification may become extremely difficult.

Let’s go back to the Item mode again. As mentioned above, Item can provide the information needed for Cell registration and the ability to replace the setting model (and has dynamic property, which can be displayed after modifying attributes). By the way, it also has the ability to calculate size. It is also encapsulated within the Item for the business. Can the rest of the cell. Delegate be set by Item?

Let’s look at the DataSource and Delegate from the point of view of Item. The DataSource faces the Cell and the Delegate faces the VC. Let’s implement the DataSource in the Item layer. What if you implement a Delegate as well? Forwarding events to VC actually works as well. The code snippet for extending Item is as follows:

/ / -- -- -- -- -- -- -- -- -- -- -- -- -- - DemoItem. H -- -- -- -- -- -- -- -- -- -- -- -- -- -- a layer of inheritance, / / doing here before this Item related ability can be design according to the abstract class, contains some abstract method / / DemoItem will succeed to the ability of the Item, @interface DemoItem: Item //... / /... @property (nonatomic, copy, nullable) void(^likingHandler)(DemoItem *item); @ the end / / -- -- -- -- -- -- -- -- -- -- -- -- -- - DemoItem. M -- -- -- -- -- -- -- -- -- -- -- -- -- -- # import "Cell. H / / in." m import Cell header files (Cell does not need to be exposed to the item. H) @ interface DemoItem()<DataSource, Delegate> // ... / /... @end @implementation DemoItem // ... / /... - (void)bindCell:(Cell *)cell atIndexPath:(NSIndexPath *)indexPath { cell.dataSource = self; cell.delegate = self; // Set cell.delegate _cell = cell; // } // Cell - Delegate methods - (void)likingItemWasTappedOnCell:(Cell *)cell { if ( _likingHandler ! = nil ) _likingHandler(self); } @end // -------------- ViewController.m -------------- @implementation ViewController // ... / /... - (void)_setupItems { // ... / /... NSArray<DemoItem *> *demoItems = ... ; for ( DemoItem *item in demoItems ) { item.likingHandler = ^(DemoItem *item) { // ... / /... BOOL isLiked = ! item.isLiked; / /... After the network request is made here, update the item property item.liked = isLiked; } } } @endCopy the code

Set the cell. Delegate inside the Item above, and provide the corresponding event as a block property. In VC, business is processed by setting item. block.

At this stage, VC may not perceive the existence of the Cell. VC only interacts with Item through block, and sets Item attributes or calls Item methods at a certain time, indirectly completing the refresh of the Cell.

Cell agent events we finished processing, here you might have thought of UICollectionView agent method collectionView: didSelectItemAtIndexPath: what should I do? The old way of writing it is that this method is going to make some judgments based on indexPath to handle the click event, and it’s going to be full of judgments as well. But here’s what it would look like to pass the click event to Item:

// -------------- Item.h -------------- @interface Item // ... / /... @property (nonatomic, copy, nullable) void(^selectionHandler)(__kindof Item *item, NSIndexPath *indexPath); @end // -------------- ViewController.m -------------- @implementation ViewController // ... / /... - (void)_setupItems { // ... / /... NSArray<DemoItem *> *demoItems = ... ; For (DemoItem *item in demoItems) {// set the click to process item.selectionHandler = ^(DemoItem *item) {//... / /... } } } - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath { // Obtain the corresponding position of the item / / itemAtIndexPath: this method only to do simulation, the subsequent will introduce how to manage and get the item the item * item = [self itemAtIndexPath: indexPath]; if ( item.selectionHandler ! = nil ) item.selectionHandler(item, indexPath); } @endCopy the code

As above, selectionHandler is added to Item, configured in the appropriate place in the VC, and finally called in the proxy method.

Level 6 Manages items -Section

Item can be said to have complete functions now, but it only represents a single individual. What we need in the future is a group or more groups of items, and a class is needed to be responsible for management (similar to array, it can provide the ability of adding, deleting and acquiring elements).

The CollectionView is made up of sections, and sections are made up of SectionHeader, Cell, SectionFooter, and decorations.

Section is responsible for managing items, and the code snippet looks like this:

@interface Section : NSObject @property (nonatomic) CGFloat minimumInteritemSpacing; @property (nonatomic) CGFloat minimumLineSpacing; @property (nonatomic) UIEdgeInsets sectionInsets; @property (nonatomic, strong, nullable) __kindof SectionHeaderFooter *header; @property (nonatomic, strong, nullable) __kindof SectionHeaderFooter *footer; @property (nonatomic, readOnly) NSInteger numberOfItems; - (NSInteger)addItem:(Item *)item; - (nullable NSIndexSet *)addItemsFromArray:(NSArray<Item *> *)items; - (NSInteger)insertItem:(Item *)item atIndex:(NSInteger)index; - (NSInteger)moveItem:(Item *)item toIndex:(NSInteger)index; - (NSInteger)removeItemAtIndex:(NSInteger)index; - (nullable NSIndexSet *)removeItemsInArray:(NSArray<Item *> *)items; - (void)removeAllItems; - (void)setNeedsLayout; / /... / /... @endCopy the code

By the way, in the code snippet above, there are not only items, but also headers and footers. Obviously, not only can you wrap a Cell with an Item, but views like HeaderFooterView can be wrapped similarly.

MinimumInteritemSpacing, minimumLineSpacing, sectionInsets and so on are attributes for layouts, and header attributes, Add, delete, change and check footer and items.

These code fragments do not represent the full capabilities of the Section.

Level 7 Manages sections -CollectionProvider

Similarly, a Section is an individual, so we need one or more groups of sections, and we need to add, delete, change, check, and so on.

Let’s look at the code snippet below:

@interface CollectionProvider : NSObject

@property (nonatomic, readonly) NSInteger numberOfSections;
- (void)addSectionWithBlock:(void(^NS_NOESCAPE)(__kindof Section *make))block;
- (NSInteger)addSection:(Section *)section;
- (nullable NSIndexSet *)addSections:(NSArray<Section *> *)sections;
- (NSInteger)insertSection:(Section *)section atIndex:(NSInteger)index;
- (void)replaceSectionAtIndex:(NSInteger)index withSection:(Section *)section;

- (nullable __kindof Section *)sectionAtIndex:(NSInteger)index;
- (nullable __kindof Item *)itemAtIndexPath:(NSIndexPath *)indexPath;
 
- (nullable NSIndexPath *)moveItemAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath;
- (nullable NSIndexPath *)insertItem:(Item *)item atIndexPath:(NSIndexPath *)indexPath;

- (NSInteger)removeSectionAtIndex:(NSInteger)index;
- (NSInteger)removeSection:(Section *)section;
- (nullable NSIndexSet *)removeSections:(NSArray<Section *> *)sections;
- (void)removeAllSections;
  
- (nullable NSIndexPath *)removeItemAtIndexPath:(NSIndexPath *)indexPath;
- (nullable NSIndexPath *)removeItem:(Item *)item;
- (nullable NSArray<NSIndexPath *> *)removeItemsInArray:(NSArray<Item *> *)items;
- (nullable NSArray<NSIndexPath *> *)removeItemsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths;
@end
Copy the code

Provider can manage sections and add, delete, modify, and check interfaces. Take a look at the interface and see if it feels like it’s providing the CollectionView data, which is why it’s called a Provider.

Beat the game – used in VC

Here, almost customs clearance, we look at the use of VC effect, the code is as follows:

@interface ViewController()<UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout> @property (nonatomic, strong) UICollectionView *collectionView; @property (nonatomic, strong) CollectionProvider *provider; @end @implementation ViewController // ... / /... - (void)_setupSections {// Add only one Section Section [_provider addSectionWithBlock:^(__kindof Section * _Nonnull make) {make. MinimumLineSpacing = 8;  make.minimumInteritemSpacing = 8; make.sectionInsets = UIEdgeInsetsMake(12, 12, 12, 12); NSArray *models = nil; For (id model in models) {DemoItem *item = [demoitem.alloc initWithModel:model];  item.selectionHandler = ^(__kindof Item * _Nonnull item, NSIndexPath * _Nonnull indexPath) { // ... // ... };  item.likingHandler = ^(DemoItem *item) { // ... // ... } [make addItem:item]; } }]; } - (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView { return _provider.numberOfSections; } - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { return [_provider sectionAtIndex:section].numberOfItems; } - (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath { [[_provider itemAtIndexPath:indexPath] bindCell:cell atIndexPath:indexPath]; } - (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath { [[_provider itemAtIndexPath:indexPath] unbindCell:cell atIndexPath:indexPath]; } - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath { Item *item = [_provider itemAtIndexPath:indexPath]; if ( item.selectionHandler ! = nil ) item.selectionHandler(item, indexPath); } - (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section { return [_provider sectionAtIndex:section].minimumLineSpacing; } - (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section { return [_provider sectionAtIndex:section].minimumInteritemSpacing; } - (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section { return [_provider sectionAtIndex:section].contentInsets; } #pragma mark **-** - (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { return nil; 1} - (CGSize)collectionView (UICollectionView *)collectionView Layout (UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath { return CGSizeZero; // The last two pits --2} @endCopy the code

Haha, there are still two holes left to fill.

We first fill out collectionView: cellForItemAtIndexPath: before, haven’t said the timing of Cell register, is here, this place is our registered the best time. This method needs to return the Cell, so we will register and return the Cell together. The code snippet is as follows:

// -------------- Register.h -------------- @interface CollectionRegister : NSObject // ... / /... // According to the Item registration information, Register and return Cell - (nullable __kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView dequeueReusableCellWithItem:(Item *)item forIndexPath:(NSIndexPath *)indexPath; @end // -------------- ViewController.m -------------- @interface ViewController()<UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout> // ... / /... @property (nonatomic, strong) CollectionRegister *collectionRegister; @end @implementation ViewController // ... / /... - (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *) indexPath {/ / registration and return the Cell return [_collectionRegister collectionView: collectionView dequeueReusableCellWithItem:[_provider itemAtIndexPath:indexPath] forIndexPath:indexPath]; } @endCopy the code

If you use the CollectionRegister to register and return cells, you might wonder if there is a problem with double registrations. Haha, it depends on how you implement this method, but the interface is there.

Followed by the last pit collectionView: layout: sizeForItemAtIndexPath:, through the practice of the above you should think I may need to jilt pit again, the code is as follows:

// -------------- CollectionSizes.h --------------
@interface CollectionSizes : NSObject
// ...
// ...
- (CGSize)collectionView:(UICollectionView *)collectionView
                  layout:(UICollectionViewLayout *)layout
             sizeForItem:(CollectionItem *)item
               inSection:(Section *)section
             atIndexPath:(NSIndexPath *)indexPath;
@end

// -------------- ViewController.m --------------
@interface ViewController()<UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout>
// ...
// ...
@property (nonatomic, strong) CollectionSizes *collectionSizes;
@end

@implementation ViewController
// ...
// ...
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
    return [_collectionSizes collectionView:collectionView layout:collectionViewLayout sizeForItem:[_provider itemAtIndexPath:indexPath] inSection:[_provider sectionAtIndex:indexPath.section] atIndexPath:indexPath];
}
@end
Copy the code

If you use CollectionSizes to evaluate and return size, you might wonder if there is a problem with double-counting. It depends on how you implement this method, but the interface is there.

Customs clearance completed.

Leave a demo: github.com/changsanjia…