Permalink
Fetching contributors…
Cannot retrieve contributors at this time
2584 lines (2215 sloc) 112 KB
//******************************************************************************
//
// UICollectionView.m
// PSPDFKit
//
// Copyright (c) 2012-2013 Peter Steinberger. All rights reserved.
//
// Copyright (c) 2015 Microsoft Corporation. All rights reserved.
// This code is licensed under the MIT License (MIT).
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
//******************************************************************************
#include <StubReturn.h>
#import <Foundation/Foundation.h>
#import <UIKit/UICollectionView.h>
#import <UIKit/UIScrollViewDelegate.h>
#import "UICollectionViewData.h"
#import "UICollectionViewLayout+Internal.h"
#include "UICollectionViewLayoutAttributes+Internal.h"
#import "UICollectionViewItemKey.h"
#import "NSLogging.h"
#import "AssertARCEnabled.h"
#import "ErrorHandling.h"
#import <MacTypes.h>
static const wchar_t* TAG = L"UICollectionView";
static CGFloat UIAnimationDragCoefficient = 1.f;
static CGFloat UISimulatorAnimationDragCoefficient(void) {
return UIAnimationDragCoefficient;
}
@interface UICollectionViewData (Internal)
- (void)prepareToLoadData;
@end
@interface UICollectionViewCell (Internal)
- (void)performSelectionSegue;
@end
@interface UICollectionViewUpdateItem ()
- (NSIndexPath*)indexPath;
- (BOOL)isSectionOperation;
@end
@interface UICollectionViewLayoutAttributes ()
@property (nonatomic, copy) NSString* elementKind;
@end
CGFloat UISimulatorAnimationDragCoefficient(void);
@interface UICollectionView ()
@property (nonatomic, strong) UICollectionViewData* collectionViewData;
@property (nonatomic, readonly) id currentUpdate;
@property (nonatomic, readonly) NSDictionary* visibleViewsDict;
@property (nonatomic, assign) CGRect visibleBoundRects;
@property (nonatomic, unsafe_unretained) id<UICollectionViewDelegate> collectionViewDelegate;
@property (nonatomic, strong) UICollectionViewLayout* nibLayout;
@property (nonatomic, strong) NSDictionary* nibCellsExternalObjects;
@property (nonatomic, strong) NSDictionary* supplementaryViewsExternalObjects;
@property (nonatomic, strong) NSIndexPath* touchingIndexPath;
@property (nonatomic, strong) NSIndexPath* currentIndexPath;
@end
static char kUIColletionViewExt;
@interface UICollectionView (Internal) <UICollectionViewDelegate, UIScrollViewDelegate>
@end
@implementation UICollectionView {
UICollectionViewLayout* _layout;
__unsafe_unretained id<UICollectionViewDataSource> _dataSource;
UIView* _backgroundView;
NSMutableSet* _indexPathsForSelectedItems;
NSMutableDictionary* _cellReuseQueues;
NSMutableDictionary* _supplementaryViewReuseQueues;
NSMutableDictionary* _decorationViewReuseQueues;
NSMutableSet* _indexPathsForHighlightedItems;
int _reloadingSuspendedCount;
UICollectionReusableView* _firstResponderView;
UIView* _newContentView;
int _firstResponderViewType;
NSString* _firstResponderViewKind;
NSIndexPath* _firstResponderIndexPath;
NSMutableDictionary* _allVisibleViewsDict;
NSIndexPath* _pendingSelectionIndexPath;
NSMutableSet* _pendingDeselectionIndexPaths;
UICollectionViewData* _collectionViewData;
id _update;
CGRect _visibleBoundRects;
CGRect _preRotationBounds;
CGPoint _rotationBoundsOffset;
int _rotationAnimationCount;
int _updateCount;
NSMutableArray* _insertItems;
NSMutableArray* _deleteItems;
NSMutableArray* _reloadItems;
NSMutableArray* _moveItems;
NSArray* _originalInsertItems;
NSArray* _originalDeleteItems;
UITouch* _currentTouch;
void (^_updateCompletionHandler)(BOOL finished);
NSMutableDictionary* _cellClassDict;
NSMutableDictionary* _cellNibDict;
NSMutableDictionary* _supplementaryViewClassDict;
NSMutableDictionary* _supplementaryViewNibDict;
NSMutableDictionary* _cellNibExternalObjectsTables;
NSMutableDictionary* _supplementaryViewNibExternalObjectsTables;
struct {
unsigned int delegateShouldHighlightItemAtIndexPath : 1;
unsigned int delegateDidHighlightItemAtIndexPath : 1;
unsigned int delegateDidUnhighlightItemAtIndexPath : 1;
unsigned int delegateShouldSelectItemAtIndexPath : 1;
unsigned int delegateShouldDeselectItemAtIndexPath : 1;
unsigned int delegateDidSelectItemAtIndexPath : 1;
unsigned int delegateDidDeselectItemAtIndexPath : 1;
unsigned int delegateSupportsMenus : 1;
unsigned int delegateDidEndDisplayingCell : 1;
unsigned int delegateDidEndDisplayingSupplementaryView : 1;
unsigned int dataSourceNumberOfSections : 1;
unsigned int dataSourceViewForSupplementaryElement : 1;
unsigned int reloadSkippedDuringSuspension : 1;
unsigned int scheduledUpdateVisibleCells : 1;
unsigned int scheduledUpdateVisibleCellLayoutAttributes : 1;
unsigned int allowsSelection : 1;
unsigned int allowsMultipleSelection : 1;
unsigned int updating : 1;
unsigned int fadeCellsForBoundsChange : 1;
unsigned int updatingLayout : 1;
unsigned int needsReload : 1;
unsigned int reloading : 1;
unsigned int skipLayoutDuringSnapshotting : 1;
unsigned int layoutInvalidatedSinceLastCellUpdate : 1;
unsigned int doneFirstLayout : 1;
} _collectionViewFlags;
CGPoint _lastLayoutOffset;
}
@synthesize collectionViewLayout = _layout;
@synthesize currentUpdate = _update;
@synthesize visibleViewsDict = _allVisibleViewsDict;
///////////////////////////////////////////////////////////////////////////////////////////
#pragma mark - NSObject
/**
@Public No
*/
- (void)_commonSetup {
_collectionViewFlags.allowsSelection = YES;
_indexPathsForSelectedItems = [NSMutableSet new];
_indexPathsForHighlightedItems = [NSMutableSet new];
_cellReuseQueues = [NSMutableDictionary new];
_supplementaryViewReuseQueues = [NSMutableDictionary new];
_decorationViewReuseQueues = [NSMutableDictionary new];
_allVisibleViewsDict = [NSMutableDictionary new];
_cellClassDict = [NSMutableDictionary new];
_cellNibDict = [NSMutableDictionary new];
_supplementaryViewClassDict = [NSMutableDictionary new];
_supplementaryViewNibDict = [NSMutableDictionary new];
}
/**
@Status Interoperable
*/
- (instancetype)initWithFrame:(CGRect)frame {
return [self initWithFrame:frame collectionViewLayout:nil];
}
/**
@Status Interoperable
*/
- (instancetype)initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout*)layout {
if ((self = [super initWithFrame:frame])) {
// Set self as the UIScrollView's delegate
[super setDelegate:self];
[self _commonSetup];
self.collectionViewLayout = layout;
_collectionViewData = [[UICollectionViewData alloc] initWithCollectionView:self layout:layout];
self.showsHorizontalScrollIndicator = FALSE;
}
return self;
}
/**
@Status Interoperable
*/
- (void)setShowsHorizontalScrollIndicator:(BOOL)show {
[super setShowsHorizontalScrollIndicator:FALSE];
}
/**
@Status Interoperable
*/
- (instancetype)initWithCoder:(NSCoder*)inCoder {
if ((self = [super initWithCoder:inCoder])) {
// Set self as the UIScrollView's delegate
[super setDelegate:self];
[self _commonSetup];
self.nibLayout = [inCoder decodeObjectForKey:@"UICollectionLayout"];
NSDictionary* cellExternalObjects = [inCoder decodeObjectForKey:@"UICollectionViewCellPrototypeNibExternalObjects"];
NSDictionary* cellNibs = [inCoder decodeObjectForKey:@"UICollectionViewCellNibDict"];
for (NSString* identifier in cellNibs.allKeys) {
_cellNibDict[identifier] = cellNibs[identifier];
}
self.nibCellsExternalObjects = cellExternalObjects;
NSDictionary* supplementaryViewExternalObjects =
[inCoder decodeObjectForKey:@"UICollectionViewSupplementaryViewPrototypeNibExternalObjects"];
NSDictionary* supplementaryViewNibs = [inCoder decodeObjectForKey:@"UICollectionViewSupplementaryViewNibDict"];
for (NSString* identifier in supplementaryViewNibs.allKeys) {
_supplementaryViewNibDict[identifier] = supplementaryViewNibs[identifier];
}
self.supplementaryViewsExternalObjects = supplementaryViewExternalObjects;
}
return self;
}
/**
@Status Interoperable
*/
- (void)awakeFromNib {
[super awakeFromNib];
UICollectionViewLayout* nibLayout = self.nibLayout;
if (nibLayout) {
self.collectionViewLayout = nibLayout;
self.nibLayout = nil;
}
}
/**
@Status Interoperable
*/
- (NSString*)description {
return [NSString stringWithFormat:@"%@ collection view layout: %@", [super description], self.collectionViewLayout];
}
///////////////////////////////////////////////////////////////////////////////////////////
#pragma mark - UIView
/**
@Status Interoperable
*/
- (void)layoutSubviews {
[super layoutSubviews];
// Adding alpha animation to make the relayouting smooth
if (_collectionViewFlags.fadeCellsForBoundsChange) {
// TODO: need kCATransitionFade
#if 0
CATransition *transition = [CATransition animation];
transition.duration = 0.25f * UISimulatorAnimationDragCoefficient();
transition.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
transition.type = kCATransitionFade;
[self.layer addAnimation:transition forKey:@"rotationAnimation"];
#endif
NSTraceWarning(TAG, @"fadeCellsForBoundsChange not supported");
}
[_collectionViewData validateLayoutInRect:self.bounds];
// update cells
if (_collectionViewFlags.fadeCellsForBoundsChange) {
[CATransaction begin];
[CATransaction setDisableActions:YES];
}
if (!_collectionViewFlags.updatingLayout)
[self updateVisibleCellsNow:YES];
if (_collectionViewFlags.fadeCellsForBoundsChange) {
[CATransaction commit];
}
// do we need to update contentSize?
CGSize contentSize = [_collectionViewData collectionViewContentRect].size;
if (!CGSizeEqualToSize(self.contentSize, contentSize)) {
self.contentSize = contentSize;
// if contentSize is different, we need to re-evaluate layout, bounds (contentOffset) might changed
[_collectionViewData validateLayoutInRect:self.bounds];
[self updateVisibleCellsNow:YES];
}
if (_backgroundView) {
_backgroundView.frame = (CGRect){.origin = self.contentOffset, .size = self.bounds.size };
}
_collectionViewFlags.fadeCellsForBoundsChange = NO;
_collectionViewFlags.doneFirstLayout = YES;
}
/**
@Status Interoperable
*/
- (void)setFrame:(CGRect)frame {
if (!CGRectEqualToRect(frame, self.frame)) {
CGRect bounds = (CGRect){.origin = self.contentOffset, .size = frame.size };
BOOL shouldInvalidate = [self.collectionViewLayout shouldInvalidateLayoutForBoundsChange:bounds];
[super setFrame:frame];
if (shouldInvalidate) {
[self invalidateLayout];
_collectionViewFlags.fadeCellsForBoundsChange = YES;
}
}
}
/**
@Status Interoperable
*/
- (void)setBounds:(CGRect)bounds {
if (!CGRectEqualToRect(bounds, self.bounds)) {
BOOL shouldInvalidate = [self.collectionViewLayout shouldInvalidateLayoutForBoundsChange:bounds];
[super setBounds:bounds];
if (shouldInvalidate) {
[self invalidateLayout];
_collectionViewFlags.fadeCellsForBoundsChange = YES;
}
}
}
///////////////////////////////////////////////////////////////////////////////////////////
#pragma mark - UIScrollViewDelegate
/**
@Public No
*/
- (void)scrollViewDidScroll:(UIScrollView*)scrollView {
id<UICollectionViewDelegate> delegate = self.collectionViewDelegate;
if ((id)delegate != self && [delegate respondsToSelector:@selector(scrollViewDidScroll:)]) {
[delegate scrollViewDidScroll:scrollView];
}
}
/**
@Public No
*/
- (void)scrollViewDidZoom:(UIScrollView*)scrollView {
id<UICollectionViewDelegate> delegate = self.collectionViewDelegate;
if ((id)delegate != self && [delegate respondsToSelector:@selector(scrollViewDidZoom:)]) {
[delegate scrollViewDidZoom:scrollView];
}
}
/**
@Public No
*/
- (void)scrollViewWillBeginDragging:(UIScrollView*)scrollView {
id<UICollectionViewDelegate> delegate = self.collectionViewDelegate;
if ((id)delegate != self && [delegate respondsToSelector:@selector(scrollViewWillBeginDragging:)]) {
[delegate scrollViewWillBeginDragging:scrollView];
}
}
/**
@Public No
*/
- (void)scrollViewWillEndDragging:(UIScrollView*)scrollView
withVelocity:(CGPoint)velocity
targetContentOffset:(inout CGPoint*)targetContentOffset {
// Let collectionViewLayout decide where to stop.
*targetContentOffset =
[[self collectionViewLayout] targetContentOffsetForProposedContentOffset:*targetContentOffset withScrollingVelocity:velocity];
id<UICollectionViewDelegate> delegate = self.collectionViewDelegate;
if ((id)delegate != self && [delegate respondsToSelector:@selector(scrollViewWillEndDragging:withVelocity:targetContentOffset:)]) {
// if collectionViewDelegate implements this method, it may modify targetContentOffset as well
[delegate scrollViewWillEndDragging:scrollView withVelocity:velocity targetContentOffset:targetContentOffset];
}
}
/**
@Public No
*/
- (void)scrollViewDidEndDragging:(UIScrollView*)scrollView willDecelerate:(BOOL)decelerate {
id<UICollectionViewDelegate> delegate = self.collectionViewDelegate;
if ((id)delegate != self && [delegate respondsToSelector:@selector(scrollViewDidEndDragging:willDecelerate:)]) {
[delegate scrollViewDidEndDragging:scrollView willDecelerate:decelerate];
}
// if we are in the middle of a cell touch event, perform the "touchEnded" simulation
if (self.touchingIndexPath) {
[self cellTouchCancelled];
}
}
/**
@Public No
*/
- (void)scrollViewWillBeginDecelerating:(UIScrollView*)scrollView {
id<UICollectionViewDelegate> delegate = self.collectionViewDelegate;
if ((id)delegate != self && [delegate respondsToSelector:@selector(scrollViewWillBeginDecelerating:)]) {
[delegate scrollViewWillBeginDecelerating:scrollView];
}
}
/**
@Public No
*/
- (void)scrollViewDidEndDecelerating:(UIScrollView*)scrollView {
id<UICollectionViewDelegate> delegate = self.collectionViewDelegate;
if ((id)delegate != self && [delegate respondsToSelector:@selector(scrollViewDidEndDecelerating:)]) {
[delegate scrollViewDidEndDecelerating:scrollView];
}
}
/**
@Public No
*/
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView*)scrollView {
id<UICollectionViewDelegate> delegate = self.collectionViewDelegate;
if ((id)delegate != self && [delegate respondsToSelector:@selector(scrollViewDidEndScrollingAnimation:)]) {
[delegate scrollViewDidEndScrollingAnimation:scrollView];
}
}
/**
@Public No
*/
- (UIView*)viewForZoomingInScrollView:(UIScrollView*)scrollView {
id<UICollectionViewDelegate> delegate = self.collectionViewDelegate;
if ((id)delegate != self && [delegate respondsToSelector:@selector(viewForZoomingInScrollView:)]) {
return [delegate viewForZoomingInScrollView:scrollView];
}
return nil;
}
/**
@Public No
*/
- (void)scrollViewWillBeginZooming:(UIScrollView*)scrollView withView:(UIView*)view {
id<UICollectionViewDelegate> delegate = self.collectionViewDelegate;
if ((id)delegate != self && [delegate respondsToSelector:@selector(scrollViewWillBeginZooming:withView:)]) {
[delegate scrollViewWillBeginZooming:scrollView withView:view];
}
}
/**
@Public No
*/
- (void)scrollViewDidEndZooming:(UIScrollView*)scrollView withView:(UIView*)view atScale:(CGFloat)scale {
id<UICollectionViewDelegate> delegate = self.collectionViewDelegate;
if ((id)delegate != self && [delegate respondsToSelector:@selector(scrollViewDidEndZooming:withView:atScale:)]) {
[delegate scrollViewDidEndZooming:scrollView withView:view atScale:scale];
}
}
/**
@Public No
*/
- (BOOL)scrollViewShouldScrollToTop:(UIScrollView*)scrollView {
id<UICollectionViewDelegate> delegate = self.collectionViewDelegate;
if ((id)delegate != self && [delegate respondsToSelector:@selector(scrollViewShouldScrollToTop:)]) {
return [delegate scrollViewShouldScrollToTop:scrollView];
}
return YES;
}
/**
@Public No
*/
- (void)scrollViewDidScrollToTop:(UIScrollView*)scrollView {
id<UICollectionViewDelegate> delegate = self.collectionViewDelegate;
if ((id)delegate != self && [delegate respondsToSelector:@selector(scrollViewDidScrollToTop:)]) {
[delegate scrollViewDidScrollToTop:scrollView];
}
}
///////////////////////////////////////////////////////////////////////////////////////////
#pragma mark - Public
/**
@Status Interoperable
*/
- (void)registerClass:(Class)cellClass forCellWithReuseIdentifier:(NSString*)identifier {
THROW_HR_IF_NULL(E_INVALIDARG, cellClass);
THROW_HR_IF_NULL(E_INVALIDARG, identifier);
_cellClassDict[identifier] = cellClass;
}
/**
@Status Interoperable
*/
- (void)registerClass:(Class)viewClass forSupplementaryViewOfKind:(NSString*)elementKind withReuseIdentifier:(NSString*)identifier {
THROW_HR_IF_NULL(E_INVALIDARG, viewClass);
THROW_HR_IF_NULL(E_INVALIDARG, elementKind);
THROW_HR_IF_NULL(E_INVALIDARG, identifier);
NSString* kindAndIdentifier = [NSString stringWithFormat:@"%@/%@", elementKind, identifier];
_supplementaryViewClassDict[kindAndIdentifier] = viewClass;
}
/**
@Status Stub
*/
- (void)registerNib:(UINib*)nib forCellWithReuseIdentifier:(NSString*)identifier {
NSArray* topLevelObjects = [nib instantiateWithOwner:nil options:nil];
#pragma unused(topLevelObjects)
THROW_HR_IF_FALSE_MSG(E_UNEXPECTED, topLevelObjects.count == 1 && [topLevelObjects[0] isKindOfClass:UICollectionViewCell.class],
"must contain exactly 1 top level object which is a UICollectionViewCell");
_cellNibDict[identifier] = nib;
}
/**
@Status Stub
*/
- (void)registerNib:(UINib*)nib forSupplementaryViewOfKind:(NSString*)kind withReuseIdentifier:(NSString*)identifier {
NSArray* topLevelObjects = [nib instantiateWithOwner:nil options:nil];
#pragma unused(topLevelObjects)
THROW_HR_IF_FALSE_MSG(E_UNEXPECTED, topLevelObjects.count == 1 && [topLevelObjects[0] isKindOfClass:UICollectionReusableView.class],
"must contain exactly 1 top level object which is a UICollectionReusableView");
NSString* kindAndIdentifier = [NSString stringWithFormat:@"%@/%@", kind, identifier];
_supplementaryViewNibDict[kindAndIdentifier] = nib;
}
/**
@Status Interoperable
*/
- (id)dequeueReusableCellWithReuseIdentifier:(NSString*)identifier forIndexPath:(NSIndexPath*)indexPath {
// de-queue cell (if available)
NSMutableArray* reusableCells = _cellReuseQueues[identifier];
UICollectionViewCell* cell = [reusableCells lastObject];
UICollectionViewLayoutAttributes* attributes = [self.collectionViewLayout layoutAttributesForItemAtIndexPath:indexPath];
if (cell) {
[reusableCells removeObjectAtIndex:reusableCells.count - 1];
} else {
if (_cellNibDict[identifier]) {
// Cell was registered via registerNib:forCellWithReuseIdentifier:
UINib* cellNib = _cellNibDict[identifier];
NSDictionary* externalObjects = self.nibCellsExternalObjects[identifier];
if (externalObjects) {
// TODO: need UiNibExternalObjects() support maybe?
// cell = [cellNib instantiateWithOwner:self options:@{UINibExternalObjects : externalObjects}][0];
cell = [cellNib instantiateWithOwner:self options:externalObjects][0];
} else {
cell = [cellNib instantiateWithOwner:self options:nil][0];
}
} else {
Class cellClass = _cellClassDict[identifier];
// compatibility layer
Class collectionViewCellClass = NSClassFromString(@"UICollectionViewCell");
if (collectionViewCellClass && [cellClass isEqual:collectionViewCellClass]) {
cellClass = UICollectionViewCell.class;
}
if (cellClass == nil) {
@throw [NSException exceptionWithName:NSInvalidArgumentException
reason:[NSString stringWithFormat:@"Class not registered for identifier %@", identifier]
userInfo:nil];
}
if (attributes) {
cell = [[cellClass alloc] initWithFrame:attributes.frame];
} else {
cell = [cellClass new];
}
}
cell.collectionView = self;
cell.reuseIdentifier = identifier;
}
[cell applyLayoutAttributes:attributes];
return cell;
}
/**
@Status Interoperable
*/
- (id)dequeueReusableSupplementaryViewOfKind:(NSString*)elementKind
withReuseIdentifier:(NSString*)identifier
forIndexPath:(NSIndexPath*)indexPath {
NSString* kindAndIdentifier = [NSString stringWithFormat:@"%@/%@", elementKind, identifier];
NSMutableArray* reusableViews = _supplementaryViewReuseQueues[kindAndIdentifier];
UICollectionReusableView* view = [reusableViews lastObject];
if (view) {
[reusableViews removeObjectAtIndex:reusableViews.count - 1];
} else {
if (_supplementaryViewNibDict[kindAndIdentifier]) {
// supplementary view was registered via registerNib:forCellWithReuseIdentifier:
UINib* supplementaryViewNib = _supplementaryViewNibDict[kindAndIdentifier];
NSDictionary* externalObjects = self.supplementaryViewsExternalObjects[kindAndIdentifier];
if (externalObjects) {
// view = [supplementaryViewNib instantiateWithOwner:self options:@{UINibExternalObjects : externalObjects}][0];
view = [supplementaryViewNib instantiateWithOwner:self options:externalObjects][0];
} else {
view = [supplementaryViewNib instantiateWithOwner:self options:nil][0];
}
} else {
Class viewClass = _supplementaryViewClassDict[kindAndIdentifier];
Class reusableViewClass = NSClassFromString(@"UICollectionReusableView");
if (reusableViewClass && [viewClass isEqual:reusableViewClass]) {
viewClass = UICollectionReusableView.class;
}
if (viewClass == nil) {
@throw [NSException
exceptionWithName:NSInvalidArgumentException
reason:[NSString stringWithFormat:@"Class not registered for kind/identifier %@", kindAndIdentifier]
userInfo:nil];
}
if (self.collectionViewLayout) {
UICollectionViewLayoutAttributes* attributes =
[self.collectionViewLayout layoutAttributesForSupplementaryViewOfKind:elementKind atIndexPath:indexPath];
if (attributes) {
view = [[viewClass alloc] initWithFrame:attributes.frame];
}
} else {
view = [viewClass new];
}
}
view.collectionView = self;
view.reuseIdentifier = identifier;
}
return view;
}
/**
@Status Caveat
@Notes This appears to be an extension.
*/
- (id)dequeueReusableOrCreateDecorationViewOfKind:(NSString*)elementKind forIndexPath:(NSIndexPath*)indexPath {
NSMutableArray* reusableViews = _decorationViewReuseQueues[elementKind];
UICollectionReusableView* view = [reusableViews lastObject];
UICollectionViewLayout* collectionViewLayout = self.collectionViewLayout;
UICollectionViewLayoutAttributes* attributes =
[collectionViewLayout layoutAttributesForDecorationViewOfKind:elementKind atIndexPath:indexPath];
if (view) {
[reusableViews removeObjectAtIndex:reusableViews.count - 1];
} else {
NSDictionary* decorationViewNibDict = collectionViewLayout.decorationViewNibDict;
NSDictionary* decorationViewExternalObjects = collectionViewLayout.decorationViewExternalObjectsTables;
if (decorationViewNibDict[elementKind]) {
// supplementary view was registered via registerNib:forCellWithReuseIdentifier:
UINib* supplementaryViewNib = decorationViewNibDict[elementKind];
NSDictionary* externalObjects = decorationViewExternalObjects[elementKind];
if (externalObjects) {
// view = [supplementaryViewNib instantiateWithOwner:self options:@{UINibExternalObjects : externalObjects}][0];
view = [supplementaryViewNib instantiateWithOwner:self options:externalObjects][0];
} else {
view = [supplementaryViewNib instantiateWithOwner:self options:nil][0];
}
} else {
NSDictionary* decorationViewClassDict = collectionViewLayout.decorationViewClassDict;
Class viewClass = decorationViewClassDict[elementKind];
Class reusableViewClass = NSClassFromString(@"UICollectionReusableView");
if (reusableViewClass && [viewClass isEqual:reusableViewClass]) {
viewClass = UICollectionReusableView.class;
}
if (viewClass == nil) {
@throw [NSException exceptionWithName:NSInvalidArgumentException
reason:[NSString stringWithFormat:@"Class not registered for identifier %@", elementKind]
userInfo:nil];
}
if (attributes) {
view = [[viewClass alloc] initWithFrame:attributes.frame];
} else {
view = [viewClass new];
}
}
view.collectionView = self;
view.reuseIdentifier = elementKind;
}
[view applyLayoutAttributes:attributes];
return view;
}
/**
@Public No
*/
- (NSArray*)allCells {
auto res = [[NSMutableArray alloc] init];
auto visibleViews = [_allVisibleViewsDict allValues];
for (id obj in visibleViews) {
if ([obj isKindOfClass:UICollectionViewCell.class]) {
[res addObject:obj];
}
}
return res;
/* WAS:
return [[_allVisibleViewsDict allValues] filteredArrayUsingPredicate:
[NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) {
return [evaluatedObject isKindOfClass:UICollectionViewCell.class];
}]];
*/
}
/**
@Status Interoperable
*/
- (NSArray*)visibleCells {
auto res = [[NSMutableArray alloc] init];
auto visibleViews = [_allVisibleViewsDict allValues];
for (id obj in visibleViews) {
if ([obj isKindOfClass:UICollectionViewCell.class] &&
CGRectIntersectsRect(self.bounds, static_cast<UICollectionViewCell*>(obj).frame)) {
[res addObject:obj];
}
}
return res;
/* WAS:
return [[_allVisibleViewsDict allValues] filteredArrayUsingPredicate:
[NSPredicate predicateWithBlock:
^BOOL(id evaluatedObject, NSDictionary *bindings) {
return [evaluatedObject isKindOfClass:UICollectionViewCell.class] &&
CGRectIntersectsRect(self.bounds, [evaluatedObject frame]);
}]];
*/
}
/**
@Status Interoperable
*/
- (void)reloadData {
if (_reloadingSuspendedCount != 0)
return;
[self invalidateLayout];
[_allVisibleViewsDict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL* stop) {
if ([obj isKindOfClass:UIView.class]) {
[obj removeFromSuperview];
}
}];
[_allVisibleViewsDict removeAllObjects];
for (NSIndexPath* indexPath in _indexPathsForSelectedItems) {
UICollectionViewCell* selectedCell = [self cellForItemAtIndexPath:indexPath];
selectedCell.selected = NO;
selectedCell.highlighted = NO;
}
[_indexPathsForSelectedItems removeAllObjects];
[_indexPathsForHighlightedItems removeAllObjects];
[self setNeedsLayout];
}
///////////////////////////////////////////////////////////////////////////////////////////
#pragma mark - Query Grid
/**
@Status Interoperable
*/
- (NSInteger)numberOfSections {
return [_collectionViewData numberOfSections];
}
/**
@Status Interoperable
*/
- (NSInteger)numberOfItemsInSection:(NSInteger)section {
return [_collectionViewData numberOfItemsInSection:section];
}
/**
@Status Interoperable
*/
- (UICollectionViewLayoutAttributes*)layoutAttributesForItemAtIndexPath:(NSIndexPath*)indexPath {
return [[self collectionViewLayout] layoutAttributesForItemAtIndexPath:indexPath];
}
/**
@Status Interoperable
*/
- (UICollectionViewLayoutAttributes*)layoutAttributesForSupplementaryElementOfKind:(NSString*)kind atIndexPath:(NSIndexPath*)indexPath {
return [[self collectionViewLayout] layoutAttributesForSupplementaryViewOfKind:kind atIndexPath:indexPath];
}
/**
@Status Interoperable
*/
- (NSIndexPath*)indexPathForItemAtPoint:(CGPoint)point {
UICollectionViewLayoutAttributes* attributes =
[[self.collectionViewLayout layoutAttributesForElementsInRect:CGRectMake(point.x, point.y, 1, 1)] lastObject];
return attributes.indexPath;
}
/**
@Status Interoperable
*/
- (NSIndexPath*)indexPathForCell:(UICollectionViewCell*)cell {
__block NSIndexPath* indexPath = nil;
[_allVisibleViewsDict enumerateKeysAndObjectsWithOptions:kNilOptions
usingBlock:^(id key, id obj, BOOL* stop) {
UICollectionViewItemKey* itemKey = (UICollectionViewItemKey*)key;
if (itemKey.type == UICollectionViewItemTypeCell) {
UICollectionViewCell* currentCell = (UICollectionViewCell*)obj;
if (currentCell == cell) {
indexPath = itemKey.indexPath;
*stop = YES;
}
}
}];
return indexPath;
}
/**
@Status Interoperable
*/
- (UICollectionViewCell*)cellForItemAtIndexPath:(NSIndexPath*)indexPath {
// NSInteger index = [_collectionViewData globalIndexForItemAtIndexPath:indexPath];
// TODO Apple uses some kind of globalIndex for this.
__block UICollectionViewCell* cell = nil;
[_allVisibleViewsDict enumerateKeysAndObjectsWithOptions:0
usingBlock:^(id key, id obj, BOOL* stop) {
UICollectionViewItemKey* itemKey = (UICollectionViewItemKey*)key;
if (itemKey.type == UICollectionViewItemTypeCell) {
if ([itemKey.indexPath isEqual:indexPath]) {
cell = obj;
*stop = YES;
}
}
}];
return cell;
}
/**
@Status Interoperable
*/
- (NSArray*)indexPathsForVisibleItems {
NSArray* visibleCells = self.visibleCells;
NSMutableArray* indexPaths = [NSMutableArray arrayWithCapacity:visibleCells.count];
[visibleCells enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL* stop) {
UICollectionViewCell* cell = (UICollectionViewCell*)obj;
[indexPaths addObject:cell.layoutAttributes.indexPath];
}];
return indexPaths;
}
/**
@Status Interoperable
*/
// returns nil or an array of selected index paths
- (NSArray*)indexPathsForSelectedItems {
return [_indexPathsForSelectedItems allObjects];
}
/**
@Status Interoperable
*/
// Interacting with the collection view.
- (void)scrollToItemAtIndexPath:(NSIndexPath*)indexPath
atScrollPosition:(UICollectionViewScrollPosition)scrollPosition
animated:(BOOL)animated {
// Ensure grid is laid out; else we can't scroll.
[self layoutSubviews];
UICollectionViewLayoutAttributes* layoutAttributes = [self.collectionViewLayout layoutAttributesForItemAtIndexPath:indexPath];
if (layoutAttributes) {
CGRect targetRect = [self makeRect:layoutAttributes.frame toScrollPosition:scrollPosition];
[self scrollRectToVisible:targetRect animated:animated];
}
}
/**
@Status Interoperable
*/
- (CGRect)makeRect:(CGRect)targetRect toScrollPosition:(UICollectionViewScrollPosition)scrollPosition {
// split parameters
NSUInteger verticalPosition = scrollPosition & 0x07; // 0000 0111
NSUInteger horizontalPosition = scrollPosition & 0x38; // 0011 1000
if (verticalPosition != UICollectionViewScrollPositionNone && verticalPosition != UICollectionViewScrollPositionTop &&
verticalPosition != UICollectionViewScrollPositionCenteredVertically && verticalPosition != UICollectionViewScrollPositionBottom) {
@throw [NSException
exceptionWithName:NSInvalidArgumentException
reason:@"UICollectionViewScrollPosition: attempt to use a scroll position with multiple vertical positioning styles"
userInfo:nil];
}
if (horizontalPosition != UICollectionViewScrollPositionNone && horizontalPosition != UICollectionViewScrollPositionLeft &&
horizontalPosition != UICollectionViewScrollPositionCenteredHorizontally &&
horizontalPosition != UICollectionViewScrollPositionRight) {
@throw [NSException
exceptionWithName:NSInvalidArgumentException
reason:
@"UICollectionViewScrollPosition: attempt to use a scroll position with multiple horizontal positioning styles"
userInfo:nil];
}
CGRect frame = self.layer.bounds;
CGFloat calculateX;
CGFloat calculateY;
switch (verticalPosition) {
case UICollectionViewScrollPositionCenteredVertically:
calculateY = fmax(targetRect.origin.y - ((frame.size.height / 2) - (targetRect.size.height / 2)), -self.contentInset.top);
targetRect = CGRectMake(targetRect.origin.x, calculateY, targetRect.size.width, frame.size.height);
break;
case UICollectionViewScrollPositionTop:
targetRect = CGRectMake(targetRect.origin.x, targetRect.origin.y, targetRect.size.width, frame.size.height);
break;
case UICollectionViewScrollPositionBottom:
calculateY = fmax(targetRect.origin.y - (frame.size.height - targetRect.size.height), -self.contentInset.top);
targetRect = CGRectMake(targetRect.origin.x, calculateY, targetRect.size.width, frame.size.height);
break;
}
switch (horizontalPosition) {
case UICollectionViewScrollPositionCenteredHorizontally:
calculateX = targetRect.origin.x - ((frame.size.width / 2) - (targetRect.size.width / 2));
targetRect = CGRectMake(calculateX, targetRect.origin.y, frame.size.width, targetRect.size.height);
break;
case UICollectionViewScrollPositionLeft:
targetRect = CGRectMake(targetRect.origin.x, targetRect.origin.y, frame.size.width, targetRect.size.height);
break;
case UICollectionViewScrollPositionRight:
calculateX = targetRect.origin.x - (frame.size.width - targetRect.size.width);
targetRect = CGRectMake(calculateX, targetRect.origin.y, frame.size.width, targetRect.size.height);
break;
}
return targetRect;
}
///////////////////////////////////////////////////////////////////////////////////////////
#pragma mark - Touch Handling
/**
@Status Interoperable
*/
- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
[super touchesBegan:touches withEvent:event];
// reset touching state vars
self.touchingIndexPath = nil;
self.currentIndexPath = nil;
CGPoint touchPoint = [[touches anyObject] locationInView:self];
NSIndexPath* indexPath = [self indexPathForItemAtPoint:touchPoint];
if (indexPath && self.allowsSelection) {
if (![self highlightItemAtIndexPath:indexPath animated:YES scrollPosition:UICollectionViewScrollPositionNone notifyDelegate:YES])
return;
self.touchingIndexPath = indexPath;
self.currentIndexPath = indexPath;
if (!self.allowsMultipleSelection) {
// temporally unhighlight background on touchesBegan (keeps selected by _indexPathsForSelectedItems)
// single-select only mode only though
NSIndexPath* tempDeselectIndexPath = _indexPathsForSelectedItems.anyObject;
if (tempDeselectIndexPath && ![tempDeselectIndexPath isEqual:self.touchingIndexPath]) {
// iOS6 UICollectionView deselects cell without notification
UICollectionViewCell* selectedCell = [self cellForItemAtIndexPath:tempDeselectIndexPath];
selectedCell.selected = NO;
}
}
}
}
/**
@Status Interoperable
*/
- (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event {
[super touchesMoved:touches withEvent:event];
// allows moving between highlight and unhighlight state only if setHighlighted is not overwritten
if (self.touchingIndexPath) {
CGPoint touchPoint = [[touches anyObject] locationInView:self];
NSIndexPath* indexPath = [self indexPathForItemAtPoint:touchPoint];
// moving out of bounds
if ([self.currentIndexPath isEqual:self.touchingIndexPath] && ![indexPath isEqual:self.touchingIndexPath] &&
[self unhighlightItemAtIndexPath:self.touchingIndexPath animated:YES notifyDelegate:YES shouldCheckHighlight:YES]) {
self.currentIndexPath = indexPath;
// moving back into the original touching cell
} else if (![self.currentIndexPath isEqual:self.touchingIndexPath] && [indexPath isEqual:self.touchingIndexPath]) {
[self highlightItemAtIndexPath:self.touchingIndexPath
animated:YES
scrollPosition:UICollectionViewScrollPositionNone
notifyDelegate:YES];
self.currentIndexPath = self.touchingIndexPath;
}
}
}
/**
@Public No
*/
- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
[super touchesEnded:touches withEvent:event];
if (self.touchingIndexPath) {
// first unhighlight the touch operation
[self unhighlightItemAtIndexPath:self.touchingIndexPath animated:YES notifyDelegate:YES];
CGPoint touchPoint = [[touches anyObject] locationInView:self];
NSIndexPath* indexPath = [self indexPathForItemAtPoint:touchPoint];
if ([indexPath isEqual:self.touchingIndexPath]) {
[self userSelectedItemAtIndexPath:indexPath];
} else if (!self.allowsMultipleSelection) {
NSIndexPath* tempDeselectIndexPath = _indexPathsForSelectedItems.anyObject;
if (tempDeselectIndexPath && ![tempDeselectIndexPath isEqual:self.touchingIndexPath]) {
[self cellTouchCancelled];
}
}
// for pedantic reasons only - always set to nil on touchesBegan
self.touchingIndexPath = nil;
self.currentIndexPath = nil;
}
}
/**
@Status Interoperable
*/
- (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event {
[super touchesCancelled:touches withEvent:event];
// do not mark touchingIndexPath as nil because whoever cancelled this touch will need to signal a touch up event later
if (self.touchingIndexPath) {
// first unhighlight the touch operation
[self unhighlightItemAtIndexPath:self.touchingIndexPath animated:YES notifyDelegate:YES];
}
}
- (void)cellTouchCancelled {
// turn on ALL the *should be selected* cells (iOS6 UICollectionView does no state keeping or other fancy optimizations)
// there should be no notifications as this is a silent "turn everything back on"
for (NSIndexPath* tempDeselectedIndexPath in [_indexPathsForSelectedItems copy]) {
UICollectionViewCell* selectedCell = [self cellForItemAtIndexPath:tempDeselectedIndexPath];
selectedCell.selected = YES;
}
}
- (void)userSelectedItemAtIndexPath:(NSIndexPath*)indexPath {
if (self.allowsMultipleSelection && [_indexPathsForSelectedItems containsObject:indexPath]) {
[self deselectItemAtIndexPath:indexPath animated:YES notifyDelegate:YES];
} else if (self.allowsSelection) {
[self selectItemAtIndexPath:indexPath animated:YES scrollPosition:UICollectionViewScrollPositionNone notifyDelegate:YES];
}
}
/**
@Status Interoperable
*/
// select item, notify delegate (internal)
- (void)selectItemAtIndexPath:(NSIndexPath*)indexPath
animated:(BOOL)animated
scrollPosition:(UICollectionViewScrollPosition)scrollPosition
notifyDelegate:(BOOL)notifyDelegate {
if (self.allowsMultipleSelection && [_indexPathsForSelectedItems containsObject:indexPath]) {
BOOL shouldDeselect = YES;
if (notifyDelegate && _collectionViewFlags.delegateShouldDeselectItemAtIndexPath) {
shouldDeselect = [self.delegate collectionView:self shouldDeselectItemAtIndexPath:indexPath];
}
if (shouldDeselect) {
[self deselectItemAtIndexPath:indexPath animated:animated notifyDelegate:notifyDelegate];
}
} else {
// either single selection, or wasn't already selected in multiple selection mode
BOOL shouldSelect = YES;
if (notifyDelegate && _collectionViewFlags.delegateShouldSelectItemAtIndexPath) {
shouldSelect = [self.delegate collectionView:self shouldSelectItemAtIndexPath:indexPath];
}
if (!self.allowsMultipleSelection) {
// now unselect the previously selected cell for single selection
NSIndexPath* tempDeselectIndexPath = _indexPathsForSelectedItems.anyObject;
if (tempDeselectIndexPath && ![tempDeselectIndexPath isEqual:indexPath]) {
[self deselectItemAtIndexPath:tempDeselectIndexPath animated:YES notifyDelegate:YES];
}
}
if (shouldSelect) {
UICollectionViewCell* selectedCell = [self cellForItemAtIndexPath:indexPath];
selectedCell.selected = YES;
[_indexPathsForSelectedItems addObject:indexPath];
[selectedCell performSelectionSegue];
if (scrollPosition != UICollectionViewScrollPositionNone) {
[self scrollToItemAtIndexPath:indexPath atScrollPosition:scrollPosition animated:animated];
}
if (notifyDelegate && _collectionViewFlags.delegateDidSelectItemAtIndexPath) {
[self.delegate collectionView:self didSelectItemAtIndexPath:indexPath];
}
}
}
}
/**
@Status Interoperable
*/
- (void)selectItemAtIndexPath:(NSIndexPath*)indexPath
animated:(BOOL)animated
scrollPosition:(UICollectionViewScrollPosition)scrollPosition {
[self selectItemAtIndexPath:indexPath animated:animated scrollPosition:scrollPosition notifyDelegate:NO];
}
/**
@Status Interoperable
*/
- (void)deselectItemAtIndexPath:(NSIndexPath*)indexPath animated:(BOOL)animated {
[self deselectItemAtIndexPath:indexPath animated:animated notifyDelegate:NO];
}
/**
@Status Interoperable
*/
- (void)deselectItemAtIndexPath:(NSIndexPath*)indexPath animated:(BOOL)animated notifyDelegate:(BOOL)notifyDelegate {
BOOL shouldDeselect = YES;
// deselect only relevant during multi mode
if (self.allowsMultipleSelection && notifyDelegate && _collectionViewFlags.delegateShouldDeselectItemAtIndexPath) {
shouldDeselect = [self.delegate collectionView:self shouldDeselectItemAtIndexPath:indexPath];
}
if (shouldDeselect && [_indexPathsForSelectedItems containsObject:indexPath]) {
UICollectionViewCell* selectedCell = [self cellForItemAtIndexPath:indexPath];
if (selectedCell) {
if (selectedCell.selected) {
selectedCell.selected = NO;
}
}
[_indexPathsForSelectedItems removeObject:indexPath];
if (notifyDelegate && _collectionViewFlags.delegateDidDeselectItemAtIndexPath) {
[self.delegate collectionView:self didDeselectItemAtIndexPath:indexPath];
}
}
}
/**
@Status Interoperable
*/
- (BOOL)highlightItemAtIndexPath:(NSIndexPath*)indexPath
animated:(BOOL)animated
scrollPosition:(UICollectionViewScrollPosition)scrollPosition
notifyDelegate:(BOOL)notifyDelegate {
BOOL shouldHighlight = YES;
if (notifyDelegate && _collectionViewFlags.delegateShouldHighlightItemAtIndexPath) {
shouldHighlight = [self.delegate collectionView:self shouldHighlightItemAtIndexPath:indexPath];
}
if (shouldHighlight) {
UICollectionViewCell* highlightedCell = [self cellForItemAtIndexPath:indexPath];
highlightedCell.highlighted = YES;
[_indexPathsForHighlightedItems addObject:indexPath];
if (scrollPosition != UICollectionViewScrollPositionNone) {
[self scrollToItemAtIndexPath:indexPath atScrollPosition:scrollPosition animated:animated];
}
if (notifyDelegate && _collectionViewFlags.delegateDidHighlightItemAtIndexPath) {
[self.delegate collectionView:self didHighlightItemAtIndexPath:indexPath];
}
}
return shouldHighlight;
}
/**
@Status Interoperable
*/
- (BOOL)unhighlightItemAtIndexPath:(NSIndexPath*)indexPath animated:(BOOL)animated notifyDelegate:(BOOL)notifyDelegate {
return [self unhighlightItemAtIndexPath:indexPath animated:animated notifyDelegate:notifyDelegate shouldCheckHighlight:NO];
}
/**
@Status Interoperable
*/
- (BOOL)unhighlightItemAtIndexPath:(NSIndexPath*)indexPath
animated:(BOOL)animated
notifyDelegate:(BOOL)notifyDelegate
shouldCheckHighlight:(BOOL)check {
if ([_indexPathsForHighlightedItems containsObject:indexPath]) {
UICollectionViewCell* highlightedCell = [self cellForItemAtIndexPath:indexPath];
// iOS6 does not notify any delegate if the cell was never highlighted (setHighlighted overwritten) during touchMoved
if (check && !highlightedCell.highlighted) {
return NO;
}
// if multiple selection or not unhighlighting a selected item we don't perform any op
if (highlightedCell.highlighted && [_indexPathsForSelectedItems containsObject:indexPath]) {
highlightedCell.highlighted = YES;
} else {
highlightedCell.highlighted = NO;
}
[_indexPathsForHighlightedItems removeObject:indexPath];
if (notifyDelegate && _collectionViewFlags.delegateDidUnhighlightItemAtIndexPath) {
[self.delegate collectionView:self didUnhighlightItemAtIndexPath:indexPath];
}
return YES;
}
return NO;
}
///////////////////////////////////////////////////////////////////////////////////////////
#pragma mark - Update Grid
/**
@Status Interoperable
*/
- (void)insertSections:(NSIndexSet*)sections {
[self updateSections:sections updateAction:UICollectionUpdateActionInsert];
}
/**
@Status Interoperable
*/
- (void)deleteSections:(NSIndexSet*)sections {
// First delete all items
NSMutableArray* paths = [NSMutableArray new];
[sections enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL* stop) {
for (int i = 0; i < [self numberOfItemsInSection:(NSInteger)idx]; ++i) {
[paths addObject:[NSIndexPath indexPathForItem:i inSection:(NSInteger)idx]];
}
}];
[self deleteItemsAtIndexPaths:paths];
// Then delete the section.
[self updateSections:sections updateAction:UICollectionUpdateActionDelete];
}
/**
@Status Interoperable
*/
- (void)reloadSections:(NSIndexSet*)sections {
[self updateSections:sections updateAction:UICollectionUpdateActionReload];
}
/**
@Status Interoperable
*/
- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection {
NSMutableArray* moveUpdateItems = [self arrayForUpdateAction:UICollectionUpdateActionMove];
[moveUpdateItems addObject:[[UICollectionViewUpdateItem alloc]
initWithInitialIndexPath:[NSIndexPath indexPathForItem:NSNotFound inSection:section]
finalIndexPath:[NSIndexPath indexPathForItem:NSNotFound inSection:newSection]
updateAction:UICollectionUpdateActionMove]];
if (!_collectionViewFlags.updating) {
[self setupCellAnimations];
[self endItemAnimations];
}
}
/**
@Status Interoperable
*/
- (void)insertItemsAtIndexPaths:(NSArray*)indexPaths {
[self updateRowsAtIndexPaths:indexPaths updateAction:UICollectionUpdateActionInsert];
}
/**
@Status Interoperable
*/
- (void)deleteItemsAtIndexPaths:(NSArray*)indexPaths {
[self updateRowsAtIndexPaths:indexPaths updateAction:UICollectionUpdateActionDelete];
}
/**
@Status Interoperable
*/
- (void)reloadItemsAtIndexPaths:(NSArray*)indexPaths {
[self updateRowsAtIndexPaths:indexPaths updateAction:UICollectionUpdateActionReload];
}
/**
@Status Interoperable
*/
- (void)moveItemAtIndexPath:(NSIndexPath*)indexPath toIndexPath:(NSIndexPath*)newIndexPath {
NSMutableArray* moveUpdateItems = [self arrayForUpdateAction:UICollectionUpdateActionMove];
[moveUpdateItems addObject:[[UICollectionViewUpdateItem alloc] initWithInitialIndexPath:indexPath
finalIndexPath:newIndexPath
updateAction:UICollectionUpdateActionMove]];
if (!_collectionViewFlags.updating) {
[self setupCellAnimations];
[self endItemAnimations];
}
}
/**
@Status Interoperable
*/
- (void)performBatchUpdates:(void (^)(void))updates completion:(void (^)(BOOL finished))completion {
[self setupCellAnimations];
if (updates)
updates();
if (completion)
_updateCompletionHandler = completion;
[self endItemAnimations];
}
///////////////////////////////////////////////////////////////////////////////////////////
#pragma mark - Properties
/**
@Status Interoperable
*/
- (void)setBackgroundView:(UIView*)backgroundView {
if (backgroundView != _backgroundView) {
[_backgroundView removeFromSuperview];
_backgroundView = backgroundView;
backgroundView.frame = (CGRect){.origin = self.contentOffset, .size = self.bounds.size };
backgroundView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
[self addSubview:backgroundView];
[self sendSubviewToBack:backgroundView];
}
}
/**
@Status Interoperable
*/
- (void)setCollectionViewLayout:(UICollectionViewLayout*)layout animated:(BOOL)animated {
if (layout == _layout)
return;
// not sure it was it original code, but here this prevents crash
// in case we switch layout before previous one was initially loaded
if (CGRectIsEmpty(self.bounds) || !_collectionViewFlags.doneFirstLayout) {
_layout.collectionView = nil;
_collectionViewData = [[UICollectionViewData alloc] initWithCollectionView:self layout:layout];
layout.collectionView = self;
_layout = layout;
// originally the use method
// _setNeedsVisibleCellsUpdate:withLayoutAttributes:
// here with CellsUpdate set to YES and LayoutAttributes parameter set to NO
// inside this method probably some flags are set and finally
// setNeedsDisplay is called
_collectionViewFlags.scheduledUpdateVisibleCells = YES;
_collectionViewFlags.scheduledUpdateVisibleCellLayoutAttributes = NO;
[self setNeedsDisplay];
} else {
layout.collectionView = self;
_layout.collectionView = nil;
_layout = layout;
_collectionViewData = [[UICollectionViewData alloc] initWithCollectionView:self layout:layout];
[_collectionViewData prepareToLoadData];
NSArray* previouslySelectedIndexPaths = [self indexPathsForSelectedItems];
NSMutableSet* selectedCellKeys = [NSMutableSet setWithCapacity:previouslySelectedIndexPaths.count];
for (NSIndexPath* indexPath in previouslySelectedIndexPaths) {
[selectedCellKeys addObject:[UICollectionViewItemKey collectionItemKeyForCellWithIndexPath:indexPath]];
}
NSArray* previouslyVisibleItemsKeys = [_allVisibleViewsDict allKeys];
NSSet* previouslyVisibleItemsKeysSet = [NSSet setWithArray:previouslyVisibleItemsKeys];
NSMutableSet* previouslyVisibleItemsKeysSetMutable = [NSMutableSet setWithArray:previouslyVisibleItemsKeys];
if ([selectedCellKeys intersectsSet:selectedCellKeys]) {
[previouslyVisibleItemsKeysSetMutable intersectSet:previouslyVisibleItemsKeysSetMutable];
}
[self bringSubviewToFront:_allVisibleViewsDict[[previouslyVisibleItemsKeysSetMutable anyObject]]];
CGPoint targetOffset = self.contentOffset;
CGPoint centerPoint =
CGPointMake(self.bounds.origin.x + self.bounds.size.width / 2.f, self.bounds.origin.y + self.bounds.size.height / 2.f);
NSIndexPath* centerItemIndexPath = [self indexPathForItemAtPoint:centerPoint];
if (!centerItemIndexPath) {
NSArray* visibleItems = [self indexPathsForVisibleItems];
if (visibleItems.count > 0) {
centerItemIndexPath = visibleItems[visibleItems.count / 2];
}
}
if (centerItemIndexPath) {
UICollectionViewLayoutAttributes* layoutAttributes = [layout layoutAttributesForItemAtIndexPath:centerItemIndexPath];
if (layoutAttributes) {
UICollectionViewScrollPosition scrollPosition =
UICollectionViewScrollPositionCenteredVertically | UICollectionViewScrollPositionCenteredHorizontally;
CGRect targetRect = [self makeRect:layoutAttributes.frame toScrollPosition:scrollPosition];
targetOffset = CGPointMake(fmax(0.f, targetRect.origin.x), fmax(0.f, targetRect.origin.y));
}
}
CGRect newlyBounds = CGRectMake(targetOffset.x, targetOffset.y, self.bounds.size.width, self.bounds.size.height);
NSArray* newlyVisibleLayoutAttrs = [_collectionViewData layoutAttributesForElementsInRect:newlyBounds];
NSMutableDictionary* layoutInterchangeData =
[NSMutableDictionary dictionaryWithCapacity:newlyVisibleLayoutAttrs.count + previouslyVisibleItemsKeysSet.count];
NSMutableSet* newlyVisibleItemsKeys = [NSMutableSet set];
for (UICollectionViewLayoutAttributes* attr in newlyVisibleLayoutAttrs) {
UICollectionViewItemKey* newKey = [UICollectionViewItemKey collectionItemKeyForLayoutAttributes:attr];
[newlyVisibleItemsKeys addObject:newKey];
UICollectionViewLayoutAttributes* prevAttr = nil;
UICollectionViewLayoutAttributes* newAttr = nil;
if (newKey.type == UICollectionViewItemTypeDecorationView) {
prevAttr = [self.collectionViewLayout layoutAttributesForDecorationViewOfKind:attr.representedElementKind
atIndexPath:newKey.indexPath];
newAttr = [layout layoutAttributesForDecorationViewOfKind:attr.representedElementKind atIndexPath:newKey.indexPath];
} else if (newKey.type == UICollectionViewItemTypeCell) {
prevAttr = [self.collectionViewLayout layoutAttributesForItemAtIndexPath:newKey.indexPath];
newAttr = [layout layoutAttributesForItemAtIndexPath:newKey.indexPath];
} else {
prevAttr = [self.collectionViewLayout layoutAttributesForSupplementaryViewOfKind:attr.representedElementKind
atIndexPath:newKey.indexPath];
newAttr = [layout layoutAttributesForSupplementaryViewOfKind:attr.representedElementKind atIndexPath:newKey.indexPath];
}
if (prevAttr != nil && newAttr != nil) {
layoutInterchangeData[newKey] = @{ @"previousLayoutInfos" : prevAttr, @"newLayoutInfos" : newAttr };
}
}
for (UICollectionViewItemKey* key in previouslyVisibleItemsKeysSet) {
UICollectionViewLayoutAttributes* prevAttr = nil;
UICollectionViewLayoutAttributes* newAttr = nil;
if (key.type == UICollectionViewItemTypeDecorationView) {
UICollectionReusableView* decorView = _allVisibleViewsDict[key];
prevAttr =
[self.collectionViewLayout layoutAttributesForDecorationViewOfKind:decorView.reuseIdentifier atIndexPath:key.indexPath];
newAttr = [layout layoutAttributesForDecorationViewOfKind:decorView.reuseIdentifier atIndexPath:key.indexPath];
} else if (key.type == UICollectionViewItemTypeCell) {
prevAttr = [self.collectionViewLayout layoutAttributesForItemAtIndexPath:key.indexPath];
newAttr = [layout layoutAttributesForItemAtIndexPath:key.indexPath];
} else if (key.type == UICollectionViewItemTypeSupplementaryView) {
UICollectionReusableView* suuplView = _allVisibleViewsDict[key];
prevAttr =
[self.collectionViewLayout layoutAttributesForSupplementaryViewOfKind:suuplView.layoutAttributes.representedElementKind
atIndexPath:key.indexPath];
newAttr = [layout layoutAttributesForSupplementaryViewOfKind:suuplView.layoutAttributes.representedElementKind
atIndexPath:key.indexPath];
}
NSMutableDictionary* layoutInterchangeDataValue = [NSMutableDictionary dictionary];
if (prevAttr)
layoutInterchangeDataValue[@"previousLayoutInfos"] = prevAttr;
if (newAttr)
layoutInterchangeDataValue[@"newLayoutInfos"] = newAttr;
layoutInterchangeData[key] = layoutInterchangeDataValue;
}
for (UICollectionViewItemKey* key in [layoutInterchangeData keyEnumerator]) {
if (key.type == UICollectionViewItemTypeCell) {
UICollectionViewCell* cell = _allVisibleViewsDict[key];
if (!cell) {
cell = [self createPreparedCellForItemAtIndexPath:key.indexPath
withLayoutAttributes:layoutInterchangeData[key][@"previousLayoutInfos"]];
_allVisibleViewsDict[key] = cell;
[self addControlledSubview:cell];
} else
[cell applyLayoutAttributes:layoutInterchangeData[key][@"previousLayoutInfos"]];
} else if (key.type == UICollectionViewItemTypeSupplementaryView) {
UICollectionReusableView* view = _allVisibleViewsDict[key];
if (!view) {
UICollectionViewLayoutAttributes* attrs = layoutInterchangeData[key][@"previousLayoutInfos"];
view = [self createPreparedSupplementaryViewForElementOfKind:attrs.representedElementKind
atIndexPath:attrs.indexPath
withLayoutAttributes:attrs];
_allVisibleViewsDict[key] = view;
[self addControlledSubview:view];
}
} else if (key.type == UICollectionViewItemTypeDecorationView) {
UICollectionReusableView* view = _allVisibleViewsDict[key];
if (!view) {
UICollectionViewLayoutAttributes* attrs = layoutInterchangeData[key][@"previousLayoutInfos"];
view = [self dequeueReusableOrCreateDecorationViewOfKind:attrs.representedElementKind forIndexPath:attrs.indexPath];
_allVisibleViewsDict[key] = view;
[self addControlledSubview:view];
}
}
};
CGRect contentRect = [_collectionViewData collectionViewContentRect];
void (^applyNewLayoutBlock)(void) = ^{
NSEnumerator* keys = [layoutInterchangeData keyEnumerator];
for (UICollectionViewItemKey* key in keys) {
// TODO: This is most likely not 100% the same time as in UICollectionView. Needs to be investigated.
UICollectionViewCell* cell = (UICollectionViewCell*)_allVisibleViewsDict[key];
[cell willTransitionFromLayout:_layout toLayout:layout];
[cell applyLayoutAttributes:layoutInterchangeData[key][@"newLayoutInfos"]];
[cell didTransitionFromLayout:_layout toLayout:layout];
}
};
void (^freeUnusedViews)(void) = ^{
NSMutableSet* toRemove = [NSMutableSet set];
for (UICollectionViewItemKey* key in [_allVisibleViewsDict keyEnumerator]) {
if (![newlyVisibleItemsKeys containsObject:key]) {
if (key.type == UICollectionViewItemTypeCell) {
[self reuseCell:_allVisibleViewsDict[key]];
[toRemove addObject:key];
} else if (key.type == UICollectionViewItemTypeSupplementaryView) {
[self reuseSupplementaryView:_allVisibleViewsDict[key]];
[toRemove addObject:key];
} else if (key.type == UICollectionViewItemTypeDecorationView) {
[self reuseDecorationView:_allVisibleViewsDict[key]];
[toRemove addObject:key];
}
}
}
for (id key in toRemove)
[_allVisibleViewsDict removeObjectForKey:key];
};
if (animated) {
[UIView animateWithDuration:.3
animations:^{
_collectionViewFlags.updatingLayout = YES;
self.contentOffset = targetOffset;
self.contentSize = contentRect.size;
applyNewLayoutBlock();
}
completion:^(BOOL finished) {
freeUnusedViews();
_collectionViewFlags.updatingLayout = NO;
// layout subviews for updating content offset or size while updating layout
if (!CGPointEqualToPoint(self.contentOffset, targetOffset) || !CGSizeEqualToSize(self.contentSize, contentRect.size)) {
[self layoutSubviews];
}
}];
} else {
self.contentOffset = targetOffset;
self.contentSize = contentRect.size;
applyNewLayoutBlock();
freeUnusedViews();
}
}
}
- (void)setCollectionViewLayout:(UICollectionViewLayout*)layout {
[self setCollectionViewLayout:layout animated:NO];
}
- (id<UICollectionViewDelegate>)delegate {
return self.collectionViewDelegate;
}
- (void)setDelegate:(id<UICollectionViewDelegate>)delegate {
self.collectionViewDelegate = delegate;
// Managing the Selected Cells
_collectionViewFlags.delegateShouldSelectItemAtIndexPath =
(unsigned int)[self.delegate respondsToSelector:@selector(collectionView:shouldSelectItemAtIndexPath:)];
_collectionViewFlags.delegateDidSelectItemAtIndexPath =
(unsigned int)[self.delegate respondsToSelector:@selector(collectionView:didSelectItemAtIndexPath:)];
_collectionViewFlags.delegateShouldDeselectItemAtIndexPath =
(unsigned int)[self.delegate respondsToSelector:@selector(collectionView:shouldDeselectItemAtIndexPath:)];
_collectionViewFlags.delegateDidDeselectItemAtIndexPath =
(unsigned int)[self.delegate respondsToSelector:@selector(collectionView:didDeselectItemAtIndexPath:)];
// Managing Cell Highlighting
_collectionViewFlags.delegateShouldHighlightItemAtIndexPath =
(unsigned int)[self.delegate respondsToSelector:@selector(collectionView:shouldHighlightItemAtIndexPath:)];
_collectionViewFlags.delegateDidHighlightItemAtIndexPath =
(unsigned int)[self.delegate respondsToSelector:@selector(collectionView:didHighlightItemAtIndexPath:)];
_collectionViewFlags.delegateDidUnhighlightItemAtIndexPath =
(unsigned int)[self.delegate respondsToSelector:@selector(collectionView:didUnhighlightItemAtIndexPath:)];
// Tracking the Removal of Views
_collectionViewFlags.delegateDidEndDisplayingCell =
(unsigned int)[self.delegate respondsToSelector:@selector(collectionView:didEndDisplayingCell:forItemAtIndexPath:)];
_collectionViewFlags.delegateDidEndDisplayingSupplementaryView = (unsigned int)[self.delegate
respondsToSelector:@selector(collectionView:didEndDisplayingSupplementaryView:forElementOfKind:atIndexPath:)];
// Managing Actions for Cells
_collectionViewFlags.delegateSupportsMenus =
(unsigned int)[self.delegate respondsToSelector:@selector(collectionView:shouldShowMenuForItemAtIndexPath:)];
// These aren't present in the flags which is a little strange. Not adding them because that will mess with byte alignment which will
// affect cross compatibility.
// The flag names are guesses and are there for documentation purposes.
// _collectionViewFlags.delegateCanPerformActionForItemAtIndexPath = [self.delegate
// respondsToSelector:@selector(collectionView:canPerformAction:forItemAtIndexPath:withSender:)];
// _collectionViewFlags.delegatePerformActionForItemAtIndexPath = [self.delegate
// respondsToSelector:@selector(collectionView:performAction:forItemAtIndexPath:withSender:)];
}
// Might be overkill since two are required and two are handled by UICollectionViewData leaving only one flag we actually need to check for
- (void)setDataSource:(id<UICollectionViewDataSource>)dataSource {
if (dataSource != _dataSource) {
_dataSource = dataSource;
// Getting Item and Section Metrics
_collectionViewFlags.dataSourceNumberOfSections =
(unsigned int)[_dataSource respondsToSelector:@selector(numberOfSectionsInCollectionView:)];
// Getting Views for Items
_collectionViewFlags.dataSourceViewForSupplementaryElement =
(unsigned int)[_dataSource respondsToSelector:@selector(collectionView:viewForSupplementaryElementOfKind:atIndexPath:)];
}
}
- (BOOL)allowsSelection {
return _collectionViewFlags.allowsSelection;
}
- (void)setAllowsSelection:(BOOL)allowsSelection {
_collectionViewFlags.allowsSelection = (unsigned int)allowsSelection;
}
- (BOOL)allowsMultipleSelection {
return _collectionViewFlags.allowsMultipleSelection;
}
- (void)setAllowsMultipleSelection:(BOOL)allowsMultipleSelection {
_collectionViewFlags.allowsMultipleSelection = (unsigned int)allowsMultipleSelection;
// Deselect all objects if allows multiple selection is false
if (!allowsMultipleSelection && _indexPathsForSelectedItems.count) {
// Note: Apple's implementation leaves a mostly random item selected. Presumably they
// have a good reason for this, but I guess it's just skipping the last or first index.
for (NSIndexPath* selectedIndexPath in [_indexPathsForSelectedItems copy]) {
if (_indexPathsForSelectedItems.count == 1)
continue;
[self deselectItemAtIndexPath:selectedIndexPath animated:YES notifyDelegate:YES];
}
}
}
- (CGRect)visibleBoundRects {
// in original UICollectionView implementation they
// check for _visibleBounds and can union self.bounds
// with this value. Don't know the meaning of _visibleBounds however.
return self.bounds;
}
///////////////////////////////////////////////////////////////////////////////////////////
#pragma mark - Private
- (void)invalidateLayout {
[self.collectionViewLayout invalidateLayout];
[self.collectionViewData invalidate]; // invalidate layout cache
}
// update currently visible cells, fetches new cells if needed
// TODO: use now parameter.
- (void)updateVisibleCellsNow:(BOOL)now {
NSArray* layoutAttributesArray = [_collectionViewData layoutAttributesForElementsInRect:self.bounds];
if (layoutAttributesArray == nil || layoutAttributesArray.count == 0) {
// If our layout source isn't providing any layout information, we should just
// stop, otherwise we'll blow away all the currently existing cells.
return;
}
// create ItemKey/Attributes dictionary
NSMutableDictionary* itemKeysToAddDict = [NSMutableDictionary dictionary];
// Add new cells.
for (UICollectionViewLayoutAttributes* layoutAttributes in layoutAttributesArray) {
UICollectionViewItemKey* itemKey = [UICollectionViewItemKey collectionItemKeyForLayoutAttributes:layoutAttributes];
itemKeysToAddDict[itemKey] = layoutAttributes;
// check if cell is in visible dict; add it if not.
UICollectionReusableView* view = _allVisibleViewsDict[itemKey];
if (!view) {
if (itemKey.type == UICollectionViewItemTypeCell) {
view = [self createPreparedCellForItemAtIndexPath:itemKey.indexPath withLayoutAttributes:layoutAttributes];
} else if (itemKey.type == UICollectionViewItemTypeSupplementaryView) {
view = [self createPreparedSupplementaryViewForElementOfKind:layoutAttributes.representedElementKind
atIndexPath:layoutAttributes.indexPath
withLayoutAttributes:layoutAttributes];
} else if (itemKey.type == UICollectionViewItemTypeDecorationView) {
view = [self dequeueReusableOrCreateDecorationViewOfKind:layoutAttributes.representedElementKind
forIndexPath:layoutAttributes.indexPath];
}
// Supplementary views are optional
if (view) {
_allVisibleViewsDict[itemKey] = view;
[self addControlledSubview:view];
// Always apply attributes. Fixes #203.
[view applyLayoutAttributes:layoutAttributes];
}
} else {
// just update cell
[view applyLayoutAttributes:layoutAttributes];
}
}
// Detect what items should be removed and queued back.
NSMutableSet* allVisibleItemKeys = [NSMutableSet setWithArray:[_allVisibleViewsDict allKeys]];
[allVisibleItemKeys minusSet:[NSSet setWithArray:[itemKeysToAddDict allKeys]]];
// Finally remove views that have not been processed and prepare them for re-use.
for (UICollectionViewItemKey* itemKey in allVisibleItemKeys) {
UICollectionReusableView* reusableView = _allVisibleViewsDict[itemKey];
if (reusableView) {
[reusableView removeFromSuperview];
[_allVisibleViewsDict removeObjectForKey:itemKey];
if (itemKey.type == UICollectionViewItemTypeCell) {
if (_collectionViewFlags.delegateDidEndDisplayingCell) {
[self.delegate collectionView:self
didEndDisplayingCell:(UICollectionViewCell*)reusableView
forItemAtIndexPath:itemKey.indexPath];
}
[self reuseCell:(UICollectionViewCell*)reusableView];
} else if (itemKey.type == UICollectionViewItemTypeSupplementaryView) {
if (_collectionViewFlags.delegateDidEndDisplayingSupplementaryView) {
[self.delegate collectionView:self
didEndDisplayingSupplementaryView:reusableView
forElementOfKind:itemKey.identifier
atIndexPath:itemKey.indexPath];
}
[self reuseSupplementaryView:reusableView];
} else if (itemKey.type == UICollectionViewItemTypeDecorationView) {
[self reuseDecorationView:reusableView];
}
}
}
}
// fetches a cell from the dataSource and sets the layoutAttributes
- (UICollectionViewCell*)createPreparedCellForItemAtIndexPath:(NSIndexPath*)indexPath
withLayoutAttributes:(UICollectionViewLayoutAttributes*)layoutAttributes {
UICollectionViewCell* cell = [self.dataSource collectionView:self cellForItemAtIndexPath:indexPath];
// Apply attributes
[cell applyLayoutAttributes:layoutAttributes];
// reset selected/highlight state
[cell setHighlighted:[_indexPathsForHighlightedItems containsObject:indexPath]];
[cell setSelected:[_indexPathsForSelectedItems containsObject:indexPath]];
// voiceover support
cell.isAccessibilityElement = YES;
return cell;
}
- (UICollectionReusableView*)createPreparedSupplementaryViewForElementOfKind:(NSString*)kind
atIndexPath:(NSIndexPath*)indexPath
withLayoutAttributes:(UICollectionViewLayoutAttributes*)layoutAttributes {
if (_collectionViewFlags.dataSourceViewForSupplementaryElement) {
UICollectionReusableView* view = [self.dataSource collectionView:self viewForSupplementaryElementOfKind:kind atIndexPath:indexPath];
[view applyLayoutAttributes:layoutAttributes];
return view;
}
return nil;
}
// @steipete optimization
- (void)queueReusableView:(UICollectionReusableView*)reusableView inQueue:(NSMutableDictionary*)queue withIdentifier:(NSString*)identifier {
THROW_HR_IF_FALSE(E_INVALIDARG, identifier.length > 0);
[reusableView removeFromSuperview];
[reusableView prepareForReuse];
// enqueue cell
NSMutableArray* reuseableViews = queue[identifier];
if (!reuseableViews) {
reuseableViews = [NSMutableArray array];
queue[identifier] = reuseableViews;
}
[reuseableViews addObject:reusableView];
}
// enqueue cell for reuse
- (void)reuseCell:(UICollectionViewCell*)cell {
[self queueReusableView:cell inQueue:_cellReuseQueues withIdentifier:cell.reuseIdentifier];
}
// enqueue supplementary view for reuse
- (void)reuseSupplementaryView:(UICollectionReusableView*)supplementaryView {
NSString* kindAndIdentifier =
[NSString stringWithFormat:@"%@/%@", supplementaryView.layoutAttributes.elementKind, supplementaryView.reuseIdentifier];
[self queueReusableView:supplementaryView inQueue:_supplementaryViewReuseQueues withIdentifier:kindAndIdentifier];
}
// enqueue decoration view for reuse
- (void)reuseDecorationView:(UICollectionReusableView*)decorationView {
[self queueReusableView:decorationView inQueue:_decorationViewReuseQueues withIdentifier:decorationView.reuseIdentifier];
}
- (void)addControlledSubview:(UICollectionReusableView*)subview {
// avoids placing views above the scroll indicator
// If the collection view is not displaying scrollIndicators then self.subviews.count can be 0.
// We take the max to ensure we insert at a non negative index because a negative index will silently fail to insert the view
NSInteger insertionIndex = MAX((NSInteger)(self.subviews.count - (self.dragging ? 1 : 0)), 0);
[self insertSubview:subview atIndex:insertionIndex];
UIView* scrollIndicatorView = nil;
if (self.dragging) {
scrollIndicatorView = [self.subviews lastObject];
}
NSMutableArray* floatingViews = [[NSMutableArray alloc] init];
for (UIView* uiView in self.subviews) {
if ([uiView isKindOfClass:UICollectionReusableView.class]) {
UICollectionReusableView* reusableView = (UICollectionReusableView*)uiView;
if (([[reusableView layoutAttributes] zIndex] > 0) || ([[reusableView layoutAttributes] isPinnedSupplementaryView])) {
[floatingViews addObject:uiView];
}
}
}
[floatingViews sortUsingComparator:^NSComparisonResult(UICollectionReusableView* obj1, UICollectionReusableView* obj2) {
BOOL isSupplementary1 = [[obj1 layoutAttributes] isPinnedSupplementaryView];
BOOL isSupplementary2 = [[obj2 layoutAttributes] isPinnedSupplementaryView];
if (isSupplementary1 != isSupplementary2) {
if (isSupplementary1) {
return (NSComparisonResult)NSOrderedDescending;
} else {
return (NSComparisonResult)NSOrderedAscending;
}
}
CGFloat z1 = [[obj1 layoutAttributes] zIndex];
CGFloat z2 = [[obj2 layoutAttributes] zIndex];
if (z1 > z2) {
return (NSComparisonResult)NSOrderedDescending;
} else if (z1 < z2) {
return (NSComparisonResult)NSOrderedAscending;
} else {
return (NSComparisonResult)NSOrderedSame;
}
}];
for (UICollectionReusableView* uiView in floatingViews) {
[self bringSubviewToFront:uiView];
}
if (floatingViews.count && scrollIndicatorView) {
[self bringSubviewToFront:scrollIndicatorView];
}
}
///////////////////////////////////////////////////////////////////////////////////////////
#pragma mark - Updating grid internal functionality
- (void)suspendReloads {
_reloadingSuspendedCount++;
}
- (void)resumeReloads {
if (0 < _reloadingSuspendedCount)
_reloadingSuspendedCount--;
}
- (NSMutableArray*)arrayForUpdateAction:(UICollectionUpdateAction)updateAction {
NSMutableArray* updateActions = nil;
switch (updateAction) {
case UICollectionUpdateActionInsert:
if (!_insertItems)
_insertItems = [NSMutableArray new];
updateActions = _insertItems;
break;
case UICollectionUpdateActionDelete:
if (!_deleteItems)
_deleteItems = [NSMutableArray new];
updateActions = _deleteItems;
break;
case UICollectionUpdateActionMove:
if (!_moveItems)
_moveItems = [NSMutableArray new];
updateActions = _moveItems;
break;
case UICollectionUpdateActionReload:
if (!_reloadItems)
_reloadItems = [NSMutableArray new];
updateActions = _reloadItems;
break;
default:
break;
}
return updateActions;
}
- (void)prepareLayoutForUpdates {
NSMutableArray* array = [[NSMutableArray alloc] init];
[array addObjectsFromArray:[_originalDeleteItems sortedArrayUsingSelector:@selector(inverseCompareIndexPaths:)]];
[array addObjectsFromArray:[_originalInsertItems sortedArrayUsingSelector:@selector(compareIndexPaths:)]];
[array addObjectsFromArray:[_reloadItems sortedArrayUsingSelector:@selector(compareIndexPaths:)]];
[array addObjectsFromArray:[_moveItems sortedArrayUsingSelector:@selector(compareIndexPaths:)]];
[_layout prepareForCollectionViewUpdates:array];
}
- (void)updateWithItems:(NSArray*)items {
[self prepareLayoutForUpdates];
NSMutableArray* animations = [[NSMutableArray alloc] init];
NSMutableDictionary* newAllVisibleView = [[NSMutableDictionary alloc] init];
NSMutableDictionary* viewsToRemove = [NSMutableDictionary dictionaryWithObjectsAndKeys:[NSMutableArray array],
@(UICollectionViewItemTypeCell),
[NSMutableArray array],
@(UICollectionViewItemTypeDecorationView),
[NSMutableArray array],
@(UICollectionViewItemTypeSupplementaryView),
nil];
for (UICollectionViewUpdateItem* updateItem in items) {
if (updateItem.isSectionOperation && updateItem.updateAction != UICollectionUpdateActionDelete)
continue;
if (updateItem.isSectionOperation && updateItem.updateAction == UICollectionUpdateActionDelete) {
NSInteger numberOfBeforeSection = [_update[@"oldModel"] numberOfItemsInSection:updateItem.indexPathBeforeUpdate.section];
for (NSInteger i = 0; i < numberOfBeforeSection; i++) {
NSIndexPath* indexPath = [NSIndexPath indexPathForItem:i inSection:updateItem.indexPathBeforeUpdate.section];
UICollectionViewLayoutAttributes* finalAttrs = [_layout finalLayoutAttributesForDisappearingItemAtIndexPath:indexPath];
UICollectionViewItemKey* key = [UICollectionViewItemKey collectionItemKeyForCellWithIndexPath:indexPath];
UICollectionReusableView* view = _allVisibleViewsDict[key];
if (view) {
UICollectionViewLayoutAttributes* startAttrs = view.layoutAttributes;
if (!finalAttrs) {
finalAttrs = [startAttrs copy];
finalAttrs.alpha = 0;
}
[animations addObject:@{ @"view" : view, @"previousLayoutInfos" : startAttrs, @"newLayoutInfos" : finalAttrs }];
[_allVisibleViewsDict removeObjectForKey:key];
[(NSMutableArray*)viewsToRemove[@(key.type)] addObject:view];
}
}
continue;
}
if (updateItem.updateAction == UICollectionUpdateActionDelete) {
NSIndexPath* indexPath = updateItem.indexPathBeforeUpdate;
UICollectionViewLayoutAttributes* finalAttrs = [_layout finalLayoutAttributesForDisappearingItemAtIndexPath:indexPath];
UICollectionViewItemKey* key = [UICollectionViewItemKey collectionItemKeyForCellWithIndexPath:indexPath];
UICollectionReusableView* view = _allVisibleViewsDict[key];
if (view) {
UICollectionViewLayoutAttributes* startAttrs = view.layoutAttributes;
if (!finalAttrs) {
finalAttrs = [startAttrs copy];
finalAttrs.alpha = 0;
}
[animations addObject:@{ @"view" : view, @"previousLayoutInfos" : startAttrs, @"newLayoutInfos" : finalAttrs }];
[_allVisibleViewsDict removeObjectForKey:key];
[(NSMutableArray*)viewsToRemove[@(key.type)] addObject:view];
}
} else if (updateItem.updateAction == UICollectionUpdateActionInsert) {
NSIndexPath* indexPath = updateItem.indexPathAfterUpdate;
UICollectionViewItemKey* key = [UICollectionViewItemKey collectionItemKeyForCellWithIndexPath:indexPath];
UICollectionViewLayoutAttributes* startAttrs = [_layout initialLayoutAttributesForAppearingItemAtIndexPath:indexPath];
UICollectionViewLayoutAttributes* finalAttrs = [_layout layoutAttributesForItemAtIndexPath:indexPath];
CGRect startRect = startAttrs.frame;
CGRect finalRect = finalAttrs.frame;
if (CGRectIntersectsRect(self.visibleBoundRects, startRect) || CGRectIntersectsRect(self.visibleBoundRects, finalRect)) {
if (!startAttrs) {
startAttrs = [finalAttrs copy];
startAttrs.alpha = 0;
}
UICollectionReusableView* view = [self createPreparedCellForItemAtIndexPath:indexPath withLayoutAttributes:startAttrs];
[self addControlledSubview:view];
newAllVisibleView[key] = view;
[animations addObject:@{ @"view" : view, @"previousLayoutInfos" : startAttrs, @"newLayoutInfos" : finalAttrs }];
}
} else if (updateItem.updateAction == UICollectionUpdateActionMove) {
NSIndexPath* indexPathBefore = updateItem.indexPathBeforeUpdate;
NSIndexPath* indexPathAfter = updateItem.indexPathAfterUpdate;
UICollectionViewItemKey* keyBefore = [UICollectionViewItemKey collectionItemKeyForCellWithIndexPath:indexPathBefore];
UICollectionViewItemKey* keyAfter = [UICollectionViewItemKey collectionItemKeyForCellWithIndexPath:indexPathAfter];
UICollectionReusableView* view = _allVisibleViewsDict[keyBefore];
UICollectionViewLayoutAttributes* startAttrs = nil;
UICollectionViewLayoutAttributes* finalAttrs = [_layout layoutAttributesForItemAtIndexPath:indexPathAfter];
if (view) {
startAttrs = view.layoutAttributes;
[_allVisibleViewsDict removeObjectForKey:keyBefore];
newAllVisibleView[keyAfter] = view;
} else {
startAttrs = [finalAttrs copy];
startAttrs.alpha = 0;
view = [self createPreparedCellForItemAtIndexPath:indexPathAfter withLayoutAttributes:startAttrs];
[self addControlledSubview:view];
newAllVisibleView[keyAfter] = view;
}
[animations addObject:@{ @"view" : view, @"previousLayoutInfos" : startAttrs, @"newLayoutInfos" : finalAttrs }];
}
}
for (UICollectionViewItemKey* key in [_allVisibleViewsDict keyEnumerator]) {
UICollectionReusableView* view = _allVisibleViewsDict[key];
if (key.type == UICollectionViewItemTypeCell) {
NSUInteger oldGlobalIndex = [_update[@"oldModel"] globalIndexForItemAtIndexPath:key.indexPath];
NSArray* oldToNewIndexMap = _update[@"oldToNewIndexMap"];
NSUInteger newGlobalIndex = NSNotFound;
if (NSNotFound != oldGlobalIndex && oldGlobalIndex < oldToNewIndexMap.count) {
newGlobalIndex = [oldToNewIndexMap[oldGlobalIndex] unsignedIntegerValue];
}
NSIndexPath* newIndexPath =
newGlobalIndex == NSNotFound ? nil : [_update[@"newModel"] indexPathForItemAtGlobalIndex:(int)newGlobalIndex];
NSIndexPath* oldIndexPath =
oldGlobalIndex == NSNotFound ? nil : [_update[@"oldModel"] indexPathForItemAtGlobalIndex:(int)oldGlobalIndex];
if (newIndexPath) {
UICollectionViewLayoutAttributes* startAttrs = nil;
UICollectionViewLayoutAttributes* finalAttrs = nil;
startAttrs = [_layout initialLayoutAttributesForAppearingItemAtIndexPath:oldIndexPath];
finalAttrs = [_layout layoutAttributesForItemAtIndexPath:newIndexPath];
NSMutableDictionary* dic = [NSMutableDictionary dictionaryWithDictionary:@{ @"view" : view }];
if (startAttrs)
dic[@"previousLayoutInfos"] = startAttrs;
if (finalAttrs)
dic[@"newLayoutInfos"] = finalAttrs;
[animations addObject:dic];
UICollectionViewItemKey* newKey = [key copy];
[newKey setIndexPath:newIndexPath];
newAllVisibleView[newKey] = view;
}
} else if (key.type == UICollectionViewItemTypeSupplementaryView) {
UICollectionViewLayoutAttributes* startAttrs = nil;
UICollectionViewLayoutAttributes* finalAttrs = nil;
startAttrs = view.layoutAttributes;
finalAttrs =
[_layout layoutAttributesForSupplementaryViewOfKind:view.layoutAttributes.representedElementKind atIndexPath:key.indexPath];
NSMutableDictionary* dic = [NSMutableDictionary dictionaryWithDictionary:@{ @"view" : view }];
if (startAttrs)
dic[@"previousLayoutInfos"] = startAttrs;
if (finalAttrs)
dic[@"newLayoutInfos"] = finalAttrs;
[animations addObject:dic];
UICollectionViewItemKey* newKey = [key copy];
newAllVisibleView[newKey] = view;
}
}
NSArray* allNewlyVisibleItems = [_layout layoutAttributesForElementsInRect:self.visibleBoundRects];
for (UICollectionViewLayoutAttributes* attrs in allNewlyVisibleItems) {
UICollectionViewItemKey* key = [UICollectionViewItemKey collectionItemKeyForLayoutAttributes:attrs];
if (key.type == UICollectionViewItemTypeCell && ![[newAllVisibleView allKeys] containsObject:key]) {
UICollectionViewLayoutAttributes* startAttrs = [_layout initialLayoutAttributesForAppearingItemAtIndexPath:attrs.indexPath];
UICollectionReusableView* view = [self createPreparedCellForItemAtIndexPath:attrs.indexPath withLayoutAttributes:startAttrs];
[self addControlledSubview:view];
newAllVisibleView[key] = view;
[animations addObject:@{ @"view" : view, @"previousLayoutInfos" : startAttrs ? startAttrs : attrs, @"newLayoutInfos" : attrs }];
}
}
// In here I think it doesn't need the animation but the transaction, it would cause some issue of display.
// I resolve the bug when user insert the new sections, the cell will display with a blink animation at the first operation.
// But I don't know why the next operation wouldn't reproduction in the pre version.
_allVisibleViewsDict = newAllVisibleView;
for (NSDictionary* animation in animations) {
UICollectionReusableView* view = animation[@"view"];
UICollectionViewLayoutAttributes* attr = animation[@"previousLayoutInfos"];
[view applyLayoutAttributes:attr];
};
_collectionViewFlags.updatingLayout = YES;
[CATransaction begin];
[CATransaction setAnimationDuration:0];
[CATransaction setCompletionBlock:^{
// Iterate through all the views that we are going to remove.
[viewsToRemove enumerateKeysAndObjectsUsingBlock:^(NSNumber* keyObj, NSArray* views, BOOL* stop) {
UICollectionViewItemType type = (UICollectionViewItemType)[keyObj unsignedIntegerValue];
for (UICollectionReusableView* view in views) {
if (type == UICollectionViewItemTypeCell) {
[self reuseCell:(UICollectionViewCell*)view];
} else if (type == UICollectionViewItemTypeSupplementaryView) {
[self reuseSupplementaryView:view];
} else if (type == UICollectionViewItemTypeDecorationView) {
[self reuseDecorationView:view];
}
}
}];
_collectionViewFlags.updatingLayout = NO;
// In here I think when the block is called, the flag is YES. So the _updateCopletionHandler's paramer is YES.
if (_updateCompletionHandler) {
_updateCompletionHandler(YES);
_updateCompletionHandler = nil;
}
}];
for (NSDictionary* animation in animations) {
UICollectionReusableView* view = animation[@"view"];
UICollectionViewLayoutAttributes* attrs = animation[@"newLayoutInfos"];
[view applyLayoutAttributes:attrs];
}
[CATransaction commit];
[_layout finalizeCollectionViewUpdates];
}
- (void)setupCellAnimations {
[self updateVisibleCellsNow:YES];
[self suspendReloads];
_collectionViewFlags.updating = YES;
}
- (void)endItemAnimations {
_updateCount++;
UICollectionViewData* oldCollectionViewData = _collectionViewData;
_collectionViewData = [[UICollectionViewData alloc] initWithCollectionView:self layout:_layout];
[_layout invalidateLayout];
[_collectionViewData prepareToLoadData];
NSMutableArray* someMutableArr1 = [[NSMutableArray alloc] init];
NSArray* removeUpdateItems =
[[self arrayForUpdateAction:UICollectionUpdateActionDelete] sortedArrayUsingSelector:@selector(inverseCompareIndexPaths:)];
NSArray* insertUpdateItems =
[[self arrayForUpdateAction:UICollectionUpdateActionInsert] sortedArrayUsingSelector:@selector(compareIndexPaths:)];
NSMutableArray* sortedMutableReloadItems = [[_reloadItems sortedArrayUsingSelector:@selector(compareIndexPaths:)] mutableCopy];
NSMutableArray* sortedMutableMoveItems = [[_moveItems sortedArrayUsingSelector:@selector(compareIndexPaths:)] mutableCopy];
_originalDeleteItems = [removeUpdateItems copy];
_originalInsertItems = [insertUpdateItems copy];
NSMutableArray* someMutableArr2 = [[NSMutableArray alloc] init];
NSMutableArray* someMutableArr3 = [[NSMutableArray alloc] init];
NSMutableDictionary* operations = [[NSMutableDictionary alloc] init];
for (UICollectionViewUpdateItem* updateItem in sortedMutableReloadItems) {
THROW_HR_IF_FALSE_MSG(E_UNEXPECTED, updateItem.indexPathBeforeUpdate.section < [oldCollectionViewData numberOfSections],
"attempt to reload item (%@) that doesn't exist (there are only %ld sections before update)",
updateItem.indexPathBeforeUpdate,
(long)[oldCollectionViewData numberOfSections]);
if (updateItem.indexPathBeforeUpdate.item != NSNotFound) {
THROW_HR_IF_FALSE_MSG(E_UNEXPECTED, updateItem.indexPathBeforeUpdate.item <
[oldCollectionViewData numberOfItemsInSection:updateItem.indexPathBeforeUpdate.section],
"attempt to reload item (%@) that doesn't exist (there are only %ld items in section %ld before update)",
updateItem.indexPathBeforeUpdate,
(long)[oldCollectionViewData numberOfItemsInSection:updateItem.indexPathBeforeUpdate.section],
(long)updateItem.indexPathBeforeUpdate.section);
}
[someMutableArr2 addObject:[[UICollectionViewUpdateItem alloc] initWithAction:UICollectionUpdateActionDelete
forIndexPath:updateItem.indexPathBeforeUpdate]];
[someMutableArr3 addObject:[[UICollectionViewUpdateItem alloc] initWithAction:UICollectionUpdateActionInsert
forIndexPath:updateItem.indexPathAfterUpdate]];
}
NSMutableArray* sortedDeletedMutableItems = [[_deleteItems sortedArrayUsingSelector:@selector(inverseCompareIndexPaths:)] mutableCopy];
NSMutableArray* sortedInsertMutableItems = [[_insertItems sortedArrayUsingSelector:@selector(compareIndexPaths:)] mutableCopy];
for (UICollectionViewUpdateItem* deleteItem in sortedDeletedMutableItems) {
if ([deleteItem isSectionOperation]) {
THROW_HR_IF_FALSE_MSG(E_UNEXPECTED, deleteItem.indexPathBeforeUpdate.section < [oldCollectionViewData numberOfSections],
"attempt to delete section (%ld) that doesn't exist (there are only %ld sections before update)",
(long)deleteItem.indexPathBeforeUpdate.section,
(long)[oldCollectionViewData numberOfSections]);
for (UICollectionViewUpdateItem* moveItem in sortedMutableMoveItems) {
if (moveItem.indexPathBeforeUpdate.section == deleteItem.indexPathBeforeUpdate.section) {
if (moveItem.isSectionOperation)
THROW_HR_IF_FALSE_MSG(E_UNEXPECTED, NO,
"attempt to delete and move from the same section %ld",
(long)deleteItem.indexPathBeforeUpdate.section);
else
THROW_HR_IF_FALSE_MSG(E_UNEXPECTED, NO, "attempt to delete and move from the same section (%@)", moveItem.indexPathBeforeUpdate);
}
}
} else {
THROW_HR_IF_FALSE_MSG(E_UNEXPECTED, deleteItem.indexPathBeforeUpdate.section < [oldCollectionViewData numberOfSections],
"attempt to delete item (%@) that doesn't exist (there are only %ld sections before update)",
deleteItem.indexPathBeforeUpdate,
(long)[oldCollectionViewData numberOfSections]);
THROW_HR_IF_FALSE_MSG(E_UNEXPECTED, deleteItem.indexPathBeforeUpdate.item <
[oldCollectionViewData numberOfItemsInSection:deleteItem.indexPathBeforeUpdate.section],
"attempt to delete item (%@) that doesn't exist (there are only %ld items in section%ld before update)",
deleteItem.indexPathBeforeUpdate,
(long)[oldCollectionViewData numberOfItemsInSection:deleteItem.indexPathBeforeUpdate.section],
(long)deleteItem.indexPathBeforeUpdate.section);
for (UICollectionViewUpdateItem* moveItem in sortedMutableMoveItems) {
THROW_HR_IF_FALSE_MSG(E_UNEXPECTED, ![deleteItem.indexPathBeforeUpdate isEqual:moveItem.indexPathBeforeUpdate],
"attempt to delete and move the same item (%@)",
deleteItem.indexPathBeforeUpdate);
}
if (!operations[@(deleteItem.indexPathBeforeUpdate.section)])
operations[@(deleteItem.indexPathBeforeUpdate.section)] = [NSMutableDictionary dictionary];
operations[@(deleteItem.indexPathBeforeUpdate.section)][@"deleted"] =
@([operations[@(deleteItem.indexPathBeforeUpdate.section)][@"deleted"] intValue] + 1);
}
}
for (NSUInteger i = 0; i < sortedInsertMutableItems.count; i++) {
UICollectionViewUpdateItem* insertItem = sortedInsertMutableItems[i];
NSIndexPath* indexPath = insertItem.indexPathAfterUpdate;
BOOL sectionOperation = [insertItem isSectionOperation];
if (sectionOperation) {
THROW_HR_IF_FALSE_MSG(E_UNEXPECTED, [indexPath section] < [_collectionViewData numberOfSections],
"attempt to insert %ld but there are only %ld sections after update",
(long)[indexPath section],
(long)[_collectionViewData numberOfSections]);
for (UICollectionViewUpdateItem* moveItem in sortedMutableMoveItems) {
if ([moveItem.indexPathAfterUpdate isEqual:indexPath]) {
if (moveItem.isSectionOperation)
THROW_HR_IF_FALSE_MSG(E_UNEXPECTED, NO, "attempt to perform an insert and a move to the same section (%ld)", (long)indexPath.section);
}
}
NSUInteger j = i + 1;
while (j < sortedInsertMutableItems.count) {
UICollectionViewUpdateItem* nextInsertItem = sortedInsertMutableItems[j];
if (nextInsertItem.indexPathAfterUpdate.section == indexPath.section) {
THROW_HR_IF_FALSE_MSG(E_UNEXPECTED, nextInsertItem.indexPathAfterUpdate.item < [_collectionViewData numberOfItemsInSection:indexPath.section],
"attempt to insert item %ld into section %ld, but there are only %ld items in section %ld after the update",
(long)nextInsertItem.indexPathAfterUpdate.item,
(long)indexPath.section,
(long)[_collectionViewData numberOfItemsInSection:indexPath.section],
(long)indexPath.section);
[sortedInsertMutableItems removeObjectAtIndex:j];
} else
break;
}
} else {
THROW_HR_IF_FALSE_MSG(E_UNEXPECTED, indexPath.item < [_collectionViewData numberOfItemsInSection:indexPath.section],
"attempt to insert item to (%@) but there are only %ld items in section %ld after update",
indexPath,
(long)[_collectionViewData numberOfItemsInSection:indexPath.section],
(long)indexPath.section);
if (!operations[@(indexPath.section)])
operations[@(indexPath.section)] = [NSMutableDictionary dictionary];
operations[@(indexPath.section)][@"inserted"] = @([operations[@(indexPath.section)][@"inserted"] intValue] + 1);
}
}
for (UICollectionViewUpdateItem* sortedItem in sortedMutableMoveItems) {
if (sortedItem.isSectionOperation) {
THROW_HR_IF_FALSE_MSG(E_UNEXPECTED, sortedItem.indexPathBeforeUpdate.section < [oldCollectionViewData numberOfSections],
"attempt to move section (%ld) that doesn't exist (%ld sections before update)",
(long)sortedItem.indexPathBeforeUpdate.section,
(long)[oldCollectionViewData numberOfSections]);
THROW_HR_IF_FALSE_MSG(E_UNEXPECTED, sortedItem.indexPathAfterUpdate.section < [_collectionViewData numberOfSections],
"attempt to move section to %ld but there are only %ld sections after update",
(long)sortedItem.indexPathAfterUpdate.section,
(long)[_collectionViewData numberOfSections]);
} else {
THROW_HR_IF_FALSE_MSG(E_UNEXPECTED, sortedItem.indexPathBeforeUpdate.section < [oldCollectionViewData numberOfSections],
"attempt to move item (%@) that doesn't exist (%ld sections before update)",
sortedItem,
(long)[oldCollectionViewData numberOfSections]);
THROW_HR_IF_FALSE_MSG(E_UNEXPECTED, sortedItem.indexPathBeforeUpdate.item <
[oldCollectionViewData numberOfItemsInSection:sortedItem.indexPathBeforeUpdate.section],
"attempt to move item (%@) that doesn't exist (%ld items in section %ld before update)",
sortedItem,
(long)[oldCollectionViewData numberOfItemsInSection:sortedItem.indexPathBeforeUpdate.section],
(long)sortedItem.indexPathBeforeUpdate.section);
THROW_HR_IF_FALSE_MSG(E_UNEXPECTED, sortedItem.indexPathAfterUpdate.section < [_collectionViewData numberOfSections],
"attempt to move item to (%@) but there are only %ld sections after update",
sortedItem.indexPathAfterUpdate,
(long)[_collectionViewData numberOfSections]);
THROW_HR_IF_FALSE_MSG(E_UNEXPECTED, sortedItem.indexPathAfterUpdate.item <
[_collectionViewData numberOfItemsInSection:sortedItem.indexPathAfterUpdate.section],
"attempt to move item to (%@) but there are only %ld items in section %ld after update",
sortedItem,
(long)[_collectionViewData numberOfItemsInSection:sortedItem.indexPathAfterUpdate.section],
(long)sortedItem.indexPathAfterUpdate.section);
}
if (!operations[@(sortedItem.indexPathBeforeUpdate.section)])
operations[@(sortedItem.indexPathBeforeUpdate.section)] = [NSMutableDictionary dictionary];
if (!operations[@(sortedItem.indexPathAfterUpdate.section)])
operations[@(sortedItem.indexPathAfterUpdate.section)] = [NSMutableDictionary dictionary];
operations[@(sortedItem.indexPathBeforeUpdate.section)][@"movedOut"] =
@([operations[@(sortedItem.indexPathBeforeUpdate.section)][@"movedOut"] intValue] + 1);
operations[@(sortedItem.indexPathAfterUpdate.section)][@"movedIn"] =
@([operations[@(sortedItem.indexPathAfterUpdate.section)][@"movedIn"] intValue] + 1);
}
#if !defined NS_BLOCK_ASSERTIONS
for (NSNumber* sectionKey in [operations keyEnumerator]) {
NSInteger section = [sectionKey integerValue];
NSInteger insertedCount = [operations[sectionKey][@"inserted"] integerValue];
NSInteger deletedCount = [operations[sectionKey][@"deleted"] integerValue];
NSInteger movedInCount = [operations[sectionKey][@"movedIn"] integerValue];
NSInteger movedOutCount = [operations[sectionKey][@"movedOut"] integerValue];
THROW_HR_IF_FALSE_MSG(E_UNEXPECTED,
[oldCollectionViewData numberOfItemsInSection:section] + insertedCount - deletedCount + movedInCount - movedOutCount ==
[_collectionViewData numberOfItemsInSection:section],
"invalid update in section %ld: number of items after update (%ld) should be equal to the number of items before update (%ld) "
"plus count of inserted items (%ld), minus count of deleted items (%ld), plus count of items moved in (%ld), minus count of "
"items moved out (%ld)",
(long)section,
(long)[_collectionViewData numberOfItemsInSection:section],
(long)[oldCollectionViewData numberOfItemsInSection:section],
(long)insertedCount,
(long)deletedCount,
(long)movedInCount,
(long)movedOutCount);
}
#endif
[someMutableArr2 addObjectsFromArray:sortedDeletedMutableItems];
[someMutableArr3 addObjectsFromArray:sortedInsertMutableItems];
[someMutableArr1 addObjectsFromArray:[someMutableArr2 sortedArrayUsingSelector:@selector(inverseCompareIndexPaths:)]];
[someMutableArr1 addObjectsFromArray:sortedMutableMoveItems];
[someMutableArr1 addObjectsFromArray:[someMutableArr3 sortedArrayUsingSelector:@selector(compareIndexPaths:)]];
NSMutableArray* layoutUpdateItems = [[NSMutableArray alloc] init];
[layoutUpdateItems addObjectsFromArray:sortedDeletedMutableItems];
[layoutUpdateItems addObjectsFromArray:sortedMutableMoveItems];
[layoutUpdateItems addObjectsFromArray:sortedInsertMutableItems];
NSMutableArray* newModel = [NSMutableArray array];
for (NSInteger i = 0; i < [oldCollectionViewData numberOfSections]; i++) {
NSMutableArray* sectionArr = [NSMutableArray array];
for (NSInteger j = 0; j < [oldCollectionViewData numberOfItemsInSection:i]; j++)
[sectionArr addObject:@([oldCollectionViewData globalIndexForItemAtIndexPath:[NSIndexPath indexPathForItem:j inSection:i]])];
[newModel addObject:sectionArr];
}
for (UICollectionViewUpdateItem* updateItem in layoutUpdateItems) {
switch (updateItem.updateAction) {
case UICollectionUpdateActionDelete: {
if (updateItem.isSectionOperation) {
// section updates are ignored anyway in animation code. If not commented, mixing rows and section deletion causes crash
// in else below
// [newModel removeObjectAtIndex:updateItem.indexPathBeforeUpdate.section];
} else {
[(NSMutableArray*)newModel[(NSUInteger)updateItem.indexPathBeforeUpdate.section]
removeObjectAtIndex:(NSUInteger)updateItem.indexPathBeforeUpdate.item];
}
} break;
case UICollectionUpdateActionInsert: {
if (updateItem.isSectionOperation) {
[newModel insertObject:[[NSMutableArray alloc] init] atIndex:(NSUInteger)updateItem.indexPathAfterUpdate.section];
} else {
[(NSMutableArray*)newModel[(NSUInteger)updateItem.indexPathAfterUpdate.section]
insertObject:@(NSNotFound)
atIndex:(NSUInteger)updateItem.indexPathAfterUpdate.item];
}
} break;
case UICollectionUpdateActionMove: {
if (updateItem.isSectionOperation) {
id section = newModel[(NSUInteger)updateItem.indexPathBeforeUpdate.section];
[newModel insertObject:section atIndex:(NSUInteger)updateItem.indexPathAfterUpdate.section];
} else {
id object = @([oldCollectionViewData globalIndexForItemAtIndexPath:updateItem.indexPathBeforeUpdate]);
[newModel[(NSUInteger)updateItem.indexPathBeforeUpdate.section] removeObject:object];
[newModel[(NSUInteger)updateItem.indexPathAfterUpdate.section]
insertObject:object
atIndex:(NSUInteger)updateItem.indexPathAfterUpdate.item];
}
} break;
default:
break;
}
}
NSMutableArray* oldToNewMap = [NSMutableArray arrayWithCapacity:(NSUInteger)[oldCollectionViewData numberOfItems]];
NSMutableArray* newToOldMap = [NSMutableArray arrayWithCapacity:(NSUInteger)[_collectionViewData numberOfItems]];
for (NSInteger i = 0; i < [oldCollectionViewData numberOfItems]; i++)
[oldToNewMap addObject:@(NSNotFound)];
for (NSInteger i = 0; i < [_collectionViewData numberOfItems]; i++)
[newToOldMap addObject:@(NSNotFound)];
for (NSUInteger i = 0; i < newModel.count; i++) {
NSMutableArray* section = newModel[i];
for (NSUInteger j = 0; j < section.count; j++) {
NSUInteger newGlobalIndex =
[_collectionViewData globalIndexForItemAtIndexPath:[NSIndexPath indexPathForItem:(NSInteger)j inSection:(NSInteger)i]];
if ([section[j] integerValue] != NSNotFound)
oldToNewMap[[section[j] unsignedIntegerValue]] = @(newGlobalIndex);
if (newGlobalIndex != NSNotFound)
newToOldMap[newGlobalIndex] = section[j];
}
}
_update = @{
@"oldModel" : oldCollectionViewData,
@"newModel" : _collectionViewData,
@"oldToNewIndexMap" : oldToNewMap,
@"newToOldIndexMap" : newToOldMap
};
[self updateWithItems:someMutableArr1];
_originalInsertItems = nil;
_originalDeleteItems = nil;
_insertItems = nil;
_deleteItems = nil;
_moveItems = nil;
_reloadItems = nil;
_update = nil;
_updateCount--;
_collectionViewFlags.updating = NO;
[self resumeReloads];
}
- (void)updateRowsAtIndexPaths:(NSArray*)indexPaths updateAction:(UICollectionUpdateAction)updateAction {
BOOL updating = _collectionViewFlags.updating;
if (!updating)
[self setupCellAnimations];
NSMutableArray* array = [self arrayForUpdateAction:updateAction]; // returns appropriate empty array if not exists
for (NSIndexPath* indexPath in indexPaths) {
UICollectionViewUpdateItem* updateItem = [[UICollectionViewUpdateItem alloc] initWithAction:updateAction forIndexPath:indexPath];
[array addObject:updateItem];
}
if (!updating)
[self endItemAnimations];
}
- (void)updateSections:(NSIndexSet*)sections updateAction:(UICollectionUpdateAction)updateAction {
BOOL updating = _collectionViewFlags.updating;
if (!updating)
[self setupCellAnimations];
NSMutableArray* updateActions = [self arrayForUpdateAction:updateAction];
[sections enumerateIndexesUsingBlock:^(NSUInteger section, BOOL* stop) {
UICollectionViewUpdateItem* updateItem =
[[UICollectionViewUpdateItem alloc] initWithAction:updateAction
forIndexPath:[NSIndexPath indexPathForItem:NSNotFound inSection:(NSInteger)section]];
[updateActions addObject:updateItem];
}];
if (!updating)
[self endItemAnimations];
}
/**
@Status Stub
*/
- (BOOL)beginInteractiveMovementForItemAtIndexPath:(NSIndexPath*)indexPath {
UNIMPLEMENTED();
return StubReturn();
}
/**
@Status Stub
*/
- (NSArray*)indexPathsForVisibleSupplementaryElementsOfKind:(NSString*)elementKind {
UNIMPLEMENTED();
return StubReturn();
}
/**
@Status Stub
*/
- (NSArray*)visibleSupplementaryViewsOfKind:(NSString*)elementKind {
UNIMPLEMENTED();
return StubReturn();
}
/**
@Status Stub
*/
- (UICollectionReusableView*)supplementaryViewForElementKind:(NSString*)elementKind atIndexPath:(NSIndexPath*)indexPath {
UNIMPLEMENTED();
return StubReturn();
}
/**
@Status Stub
*/
- (UICollectionViewTransitionLayout*)
startInteractiveTransitionToCollectionViewLayout:(UICollectionViewLayout*)layout
completion:(UICollectionViewLayoutInteractiveTransitionCompletion)completion {
UNIMPLEMENTED();
return StubReturn();
}
/**
@Status Stub
*/
- (void)cancelInteractiveMovement {
UNIMPLEMENTED();
}
/**
@Status Stub
*/
- (void)cancelInteractiveTransition {
UNIMPLEMENTED();
}
/**
@Status Stub
*/
- (void)endInteractiveMovement {
UNIMPLEMENTED();
}
/**
@Status Stub
*/
- (void)finishInteractiveTransition {
UNIMPLEMENTED();
}
/**
@Status Stub
*/
- (void)setCollectionViewLayout:(UICollectionViewLayout*)layout animated:(BOOL)animated completion:(void (^)(BOOL))completion {
UNIMPLEMENTED();
}
/**
@Status Stub
*/
- (void)updateInteractiveMovementTargetPosition:(CGPoint)targetPosition {
UNIMPLEMENTED();
}
@end