ComponentKit/DataSources/CKCollectionViewDataSource.mm (266 lines of code) (raw):

/* * Copyright (c) 2014-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. * */ #import "CKCollectionViewDataSource.h" #import "CKCollectionViewDataSourceCell.h" #import "CKDataSourceConfigurationInternal.h" #import "CKDataSourceListener.h" #import "CKDataSourceItem.h" #import "CKDataSourceState.h" #import "CKDataSourceAppliedChanges.h" #import "CKDataSourceInternal.h" #import "CKCollectionViewDataSourceInternal.h" #import "CKComponentRootViewInternal.h" #import "CKComponentLayout.h" #import "CKComponentAttachController.h" #import "CKComponentBoundsAnimation+UICollectionView.h" #import "CKComponentControllerEvents.h" #import "CKCollectionViewDataSourceListenerAnnouncer.h" @interface CKCollectionViewDataSource () <UICollectionViewDataSource, CKDataSourceListener> { CKDataSource *_componentDataSource; __weak id<CKSupplementaryViewDataSource> _supplementaryViewDataSource; CKDataSourceState *_currentState; CKComponentAttachController *_attachController; NSMapTable<UICollectionViewCell *, CKDataSourceItem *> *_cellToItemMap; CKCollectionViewDataSourceListenerAnnouncer *_announcer; BOOL _allowTapPassthroughForCells; } @end @implementation CKCollectionViewDataSource @synthesize supplementaryViewDataSource = _supplementaryViewDataSource; - (instancetype)initWithCollectionView:(UICollectionView *)collectionView supplementaryViewDataSource:(id<CKSupplementaryViewDataSource>)supplementaryViewDataSource configuration:(CKDataSourceConfiguration *)configuration { self = [super init]; if (self) { _componentDataSource = [[CKDataSource alloc] initWithConfiguration:configuration]; [_componentDataSource addListener:self]; _collectionView = collectionView; _collectionView.dataSource = self; [_collectionView registerClass:[CKCollectionViewDataSourceCell class] forCellWithReuseIdentifier:kReuseIdentifier]; _attachController = [CKComponentAttachController new]; _supplementaryViewDataSource = supplementaryViewDataSource; _cellToItemMap = [NSMapTable weakToStrongObjectsMapTable]; _announcer = [CKCollectionViewDataSourceListenerAnnouncer new]; } return self; } - (CKDataSourceState *)currentState { RCAssertMainThread(); return _currentState; } #pragma mark - Changeset application - (void)applyChangeset:(CKDataSourceChangeset *)changeset mode:(CKUpdateMode)mode userInfo:(NSDictionary *)userInfo { [_componentDataSource setTraitCollection:_collectionView.traitCollection]; [_componentDataSource applyChangeset:changeset mode:mode userInfo:userInfo]; } static void applyChangesToCollectionView(UICollectionView *collectionView, CKComponentAttachController *attachController, NSMapTable<UICollectionViewCell *, CKDataSourceItem *> *cellToItemMap, CKDataSourceState *currentState, CKDataSourceAppliedChanges *changes) { [changes.updatedIndexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, BOOL *stop) { if (CKCollectionViewDataSourceCell *cell = (CKCollectionViewDataSourceCell *) [collectionView cellForItemAtIndexPath:indexPath]) { attachToCell(cell, [currentState objectAtIndexPath:indexPath], attachController, cellToItemMap); } }]; [collectionView deleteItemsAtIndexPaths:[changes.removedIndexPaths allObjects]]; [collectionView deleteSections:changes.removedSections]; for (NSIndexPath *from in changes.movedIndexPaths) { NSIndexPath *to = changes.movedIndexPaths[from]; [collectionView moveItemAtIndexPath:from toIndexPath:to]; } [collectionView insertSections:changes.insertedSections]; [collectionView insertItemsAtIndexPaths:[changes.insertedIndexPaths allObjects]]; } #pragma mark - CKDataSourceListener - (void)dataSource:(CKDataSource *)dataSource didModifyPreviousState:(CKDataSourceState *)previousState withState:(CKDataSourceState *)state byApplyingChanges:(CKDataSourceAppliedChanges *)changes { [_announcer dataSourceWillBeginUpdates:self]; const BOOL changesIncludeNonUpdates = (changes.removedIndexPaths.count || changes.insertedIndexPaths.count || changes.movedIndexPaths.count || changes.insertedSections.count || changes.removedSections.count); const BOOL changesIncludeOnlyUpdates = (changes.updatedIndexPaths.count && !changesIncludeNonUpdates); if (changesIncludeOnlyUpdates) { // We are not able to animate the updates individually, so we pick the // first bounds animation with a non-zero duration. CKComponentBoundsAnimation boundsAnimation = {}; for (NSIndexPath *indexPath in changes.updatedIndexPaths) { boundsAnimation = [[state objectAtIndexPath:indexPath] boundsAnimation]; if (boundsAnimation.duration) break; } void (^applyUpdatedState)(CKDataSourceState *) = ^(CKDataSourceState *updatedState) { [_collectionView performBatchUpdates:^{ _currentState = updatedState; } completion:^(BOOL finished) { [_announcer dataSourceDidEndUpdates:self didModifyPreviousState:previousState withState:state byApplyingChanges:changes]; }]; }; // We only apply the bounds animation if we found one with a duration. // Animating the collection view is an expensive operation and should be // avoided when possible. if (boundsAnimation.duration) { id boundsAnimationContext = CKComponentBoundsAnimationPrepareForCollectionViewBatchUpdates(_collectionView, heightChange(previousState, state, changes.updatedIndexPaths)); [UIView performWithoutAnimation:^{ applyUpdatedState(state); }]; CKComponentBoundsAnimationApplyAfterCollectionViewBatchUpdates(boundsAnimationContext, boundsAnimation); } else { applyUpdatedState(state); } // Within an animation block we directly attach the updated items to // their respective cells if visible. CKComponentBoundsAnimationApply(boundsAnimation, ^{ for (NSIndexPath *indexPath in changes.updatedIndexPaths) { CKDataSourceItem *item = [state objectAtIndexPath:indexPath]; CKCollectionViewDataSourceCell *cell = (CKCollectionViewDataSourceCell *)[_collectionView cellForItemAtIndexPath:indexPath]; if (cell) { attachToCell(cell, item, _attachController, _cellToItemMap); } } }, nil); } else if (changesIncludeNonUpdates) { [_collectionView performBatchUpdates:^{ applyChangesToCollectionView(_collectionView, _attachController, _cellToItemMap, state, changes); // Detach all the component layouts for items being deleted [self _detachComponentLayoutForRemovedItemsAtIndexPaths:[changes removedIndexPaths] inState:previousState]; [self _detachComponentLayoutForRemovedSections:[changes removedSections] inState:previousState]; // Update current state _currentState = state; } completion:^(BOOL finished){ [_announcer dataSourceDidEndUpdates:self didModifyPreviousState:previousState withState:state byApplyingChanges:changes]; }]; } } static auto heightChange(CKDataSourceState *previousState, CKDataSourceState *state, NSSet *updatedIndexPaths) -> CGFloat { auto change = 0.0; for (NSIndexPath *indexPath in updatedIndexPaths) { auto const oldHeight = [previousState objectAtIndexPath:indexPath].rootLayout.size().height; auto const newHeight = [state objectAtIndexPath:indexPath].rootLayout.size().height; change += (newHeight - oldHeight); } return change; } - (void)dataSource:(CKDataSource *)dataSource willApplyDeferredChangeset:(CKDataSourceChangeset *)deferredChangeset {} - (void)_detachComponentLayoutForRemovedItemsAtIndexPaths:(NSSet *)removedIndexPaths inState:(CKDataSourceState *)state { for (NSIndexPath *indexPath in removedIndexPaths) { CKComponentScopeRootIdentifier identifier = [[[state objectAtIndexPath:indexPath] scopeRoot] globalIdentifier]; [_attachController detachComponentLayoutWithScopeIdentifier:identifier]; } } - (void)_detachComponentLayoutForRemovedSections:(NSIndexSet *)removedSections inState:(CKDataSourceState *)state { [removedSections enumerateIndexesUsingBlock:^(NSUInteger section, BOOL *stop) { [state enumerateObjectsInSectionAtIndex:section usingBlock:^(CKDataSourceItem *item, NSIndexPath *indexPath, BOOL *stop2) { [_attachController detachComponentLayoutWithScopeIdentifier:[[item scopeRoot] globalIdentifier]]; }]; }]; } #pragma mark - State - (id<NSObject>)modelForItemAtIndexPath:(NSIndexPath *)indexPath { return [_currentState objectAtIndexPath:indexPath].model; } - (CGSize)sizeForItemAtIndexPath:(NSIndexPath *)indexPath { return [_currentState objectAtIndexPath:indexPath].rootLayout.size(); } #pragma mark - Reload - (void)reloadWithMode:(CKUpdateMode)mode userInfo:(NSDictionary *)userInfo { [_componentDataSource setTraitCollection:_collectionView.traitCollection]; [_componentDataSource reloadWithMode:mode userInfo:userInfo]; } - (void)updateConfiguration:(CKDataSourceConfiguration *)configuration mode:(CKUpdateMode)mode userInfo:(NSDictionary *)userInfo { [_componentDataSource setTraitCollection:_collectionView.traitCollection]; [_componentDataSource updateConfiguration:configuration mode:mode userInfo:userInfo]; } #pragma mark - Appearance announcements - (void)announceWillDisplayCell:(UICollectionViewCell *)cell { CKComponentScopeRootAnnounceControllerAppearance([_cellToItemMap objectForKey:cell].scopeRoot); } - (void)announceDidEndDisplayingCell:(UICollectionViewCell *)cell { CKComponentScopeRootAnnounceControllerDisappearance([_cellToItemMap objectForKey:cell].scopeRoot); } #pragma mark - UICollectionViewDataSource static NSString *const kReuseIdentifier = @"com.component_kit.collection_view_data_source.cell"; - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { CKCollectionViewDataSourceCell *cell = [_collectionView dequeueReusableCellWithReuseIdentifier:kReuseIdentifier forIndexPath:indexPath]; [cell.rootView setAllowTapPassthrough:_allowTapPassthroughForCells]; attachToCell(cell, [_currentState objectAtIndexPath:indexPath], _attachController, _cellToItemMap); return cell; } - (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath { return [_supplementaryViewDataSource collectionView:collectionView viewForSupplementaryElementOfKind:kind atIndexPath:indexPath]; } - (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView { return _currentState ? [_currentState numberOfSections] : 0; } - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { return _currentState ? [_currentState numberOfObjectsInSection:section] : 0; } static void attachToCell(CKCollectionViewDataSourceCell *cell, CKDataSourceItem *item, CKComponentAttachController *attachController, NSMapTable<UICollectionViewCell *, CKDataSourceItem *> *cellToItemMap) { CKComponentAttachControllerAttachComponentRootLayout( attachController, {.layoutProvider = item, .scopeIdentifier = [item.scopeRoot globalIdentifier], .boundsAnimation = item.boundsAnimation, .view = cell.rootView, .analyticsListener = [item.scopeRoot analyticsListener]}); [cellToItemMap setObject:item forKey:cell]; } #pragma mark - Internal - (void)setAllowTapPassthroughForCells:(BOOL)allowTapPassthroughForCells { RCAssertMainThread(); _allowTapPassthroughForCells = allowTapPassthroughForCells; } - (void)setState:(CKDataSourceState *)state { RCAssertMainThread(); if (_currentState == state) { return; } auto const previousState = _currentState; [_announcer dataSource:self willChangeState:previousState]; _currentState = state; [_attachController detachAll]; [_componentDataSource removeListener:self]; _componentDataSource = [[CKDataSource alloc] initWithState:state]; [_componentDataSource addListener:self]; [_collectionView reloadData]; [_announcer dataSource:self didChangeState:previousState withState:state]; } - (void)addListener:(id<CKCollectionViewDataSourceListener>)listener { [_announcer addListener:listener]; } - (void)removeListener:(id<CKCollectionViewDataSourceListener>)listener { [_announcer removeListener:listener]; } @end