From ab1552b2d20b87dc14eb0c00108e8877fdebbfc7 Mon Sep 17 00:00:00 2001 From: Matt Galloway Date: Wed, 23 May 2012 10:25:27 +0100 Subject: [PATCH 01/55] ARC-ify --- PSCollectionView.h | 12 ++++++------ PSCollectionView.m | 27 ++++++--------------------- PSCollectionViewCell.h | 2 +- PSCollectionViewCell.m | 4 ---- 4 files changed, 13 insertions(+), 32 deletions(-) diff --git a/PSCollectionView.h b/PSCollectionView.h index 0bbe669..0b1a5fb 100644 --- a/PSCollectionView.h +++ b/PSCollectionView.h @@ -31,17 +31,17 @@ #pragma mark - Public Properties -@property (nonatomic, retain) UIView *headerView; -@property (nonatomic, retain) UIView *footerView; -@property (nonatomic, retain) UIView *emptyView; -@property (nonatomic, retain) UIView *loadingView; +@property (nonatomic, strong) UIView *headerView; +@property (nonatomic, strong) UIView *footerView; +@property (nonatomic, strong) UIView *emptyView; +@property (nonatomic, strong) UIView *loadingView; @property (nonatomic, assign, readonly) CGFloat colWidth; @property (nonatomic, assign, readonly) NSInteger numCols; @property (nonatomic, assign) NSInteger numColsLandscape; @property (nonatomic, assign) NSInteger numColsPortrait; -@property (nonatomic, assign) id collectionViewDelegate; -@property (nonatomic, assign) id collectionViewDataSource; +@property (nonatomic, unsafe_unretained) id collectionViewDelegate; +@property (nonatomic, unsafe_unretained) id collectionViewDataSource; #pragma mark - Public Methods diff --git a/PSCollectionView.m b/PSCollectionView.m index 768c76c..689867a 100644 --- a/PSCollectionView.m +++ b/PSCollectionView.m @@ -115,10 +115,10 @@ @interface PSCollectionView () @property (nonatomic, assign, readwrite) NSInteger numCols; @property (nonatomic, assign) UIInterfaceOrientation orientation; -@property (nonatomic, retain) NSMutableSet *reuseableViews; -@property (nonatomic, retain) NSMutableDictionary *visibleViews; -@property (nonatomic, retain) NSMutableArray *viewKeysToRemove; -@property (nonatomic, retain) NSMutableDictionary *indexToRectMap; +@property (nonatomic, strong) NSMutableSet *reuseableViews; +@property (nonatomic, strong) NSMutableDictionary *visibleViews; +@property (nonatomic, strong) NSMutableArray *viewKeysToRemove; +@property (nonatomic, strong) NSMutableDictionary *indexToRectMap; /** @@ -191,18 +191,6 @@ - (void)dealloc { self.delegate = nil; self.collectionViewDataSource = nil; self.collectionViewDelegate = nil; - - // release retains - self.headerView = nil; - self.footerView = nil; - self.emptyView = nil; - self.loadingView = nil; - - self.reuseableViews = nil; - self.visibleViews = nil; - self.viewKeysToRemove = nil; - self.indexToRectMap = nil; - [super dealloc]; } #pragma mark - Setters @@ -211,8 +199,7 @@ - (void)setLoadingView:(UIView *)loadingView { if (_loadingView && [_loadingView respondsToSelector:@selector(removeFromSuperview)]) { [_loadingView removeFromSuperview]; } - [_loadingView release], _loadingView = nil; - _loadingView = [loadingView retain]; + _loadingView = loadingView; [self addSubview:_loadingView]; } @@ -404,7 +391,7 @@ - (void)removeAndAddCellsIfNecessary { // Setup gesture recognizer if ([newView.gestureRecognizers count] == 0) { - PSCollectionViewTapGestureRecognizer *gr = [[[PSCollectionViewTapGestureRecognizer alloc] initWithTarget:self action:@selector(didSelectView:)] autorelease]; + PSCollectionViewTapGestureRecognizer *gr = [[PSCollectionViewTapGestureRecognizer alloc] initWithTarget:self action:@selector(didSelectView:)]; gr.delegate = self; [newView addGestureRecognizer:gr]; newView.userInteractionEnabled = YES; @@ -421,9 +408,7 @@ - (PSCollectionViewCell *)dequeueReusableView { PSCollectionViewCell *view = [self.reuseableViews anyObject]; if (view) { // Found a reusable view, remove it from the set - [view retain]; [self.reuseableViews removeObject:view]; - [view autorelease]; } return view; diff --git a/PSCollectionViewCell.h b/PSCollectionViewCell.h index b37551b..c9ba811 100644 --- a/PSCollectionViewCell.h +++ b/PSCollectionViewCell.h @@ -25,7 +25,7 @@ @interface PSCollectionViewCell : UIView -@property (nonatomic, retain) id object; +@property (nonatomic, strong) id object; - (void)prepareForReuse; - (void)fillViewWithObject:(id)object; diff --git a/PSCollectionViewCell.m b/PSCollectionViewCell.m index 4ffb17f..ff047ec 100644 --- a/PSCollectionViewCell.m +++ b/PSCollectionViewCell.m @@ -39,10 +39,6 @@ - (id)initWithFrame:(CGRect)frame { return self; } -- (void)dealloc { - self.object = nil; - [super dealloc]; -} - (void)prepareForReuse { } From 19c00081e10a65d0453eccb430acb1ac4242aaef Mon Sep 17 00:00:00 2001 From: Peter Shih Date: Fri, 25 May 2012 00:06:38 -0400 Subject: [PATCH 02/55] fixing some margins --- PSCollectionView.m | 3 --- 1 file changed, 3 deletions(-) diff --git a/PSCollectionView.m b/PSCollectionView.m index 689867a..881e650 100644 --- a/PSCollectionView.m +++ b/PSCollectionView.m @@ -249,11 +249,9 @@ - (void)relayoutViews { // Add headerView if it exists if (self.headerView) { - self.headerView.top = kMargin; top = self.headerView.top; [self addSubview:self.headerView]; top += self.headerView.height; - top += kMargin; } if (numViews > 0) { @@ -323,7 +321,6 @@ - (void)relayoutViews { self.footerView.top = totalHeight; [self addSubview:self.footerView]; totalHeight += self.footerView.height; - totalHeight += kMargin; } self.contentSize = CGSizeMake(self.width, totalHeight); From abad0fd09e3013b67bce4ad885dae3372a029429 Mon Sep 17 00:00:00 2001 From: Peter Shih Date: Fri, 25 May 2012 00:07:08 -0400 Subject: [PATCH 03/55] adding explicit assigns to primitive properties --- PSCollectionView.m | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/PSCollectionView.m b/PSCollectionView.m index 881e650..4519676 100644 --- a/PSCollectionView.m +++ b/PSCollectionView.m @@ -38,12 +38,12 @@ static inline NSInteger PSCollectionIndexForKey(NSString *key) { @interface UIView (PSCollectionView) -@property(nonatomic) CGFloat left; -@property(nonatomic) CGFloat top; -@property(nonatomic, readonly) CGFloat right; -@property(nonatomic, readonly) CGFloat bottom; -@property(nonatomic) CGFloat width; -@property(nonatomic) CGFloat height; +@property(nonatomic, assign) CGFloat left; +@property(nonatomic, assign) CGFloat top; +@property(nonatomic, assign, readonly) CGFloat right; +@property(nonatomic, assign, readonly) CGFloat bottom; +@property(nonatomic, assign) CGFloat width; +@property(nonatomic, assign) CGFloat height; @end From 64724461941284f409f2f5b319d52e7bed3a37e1 Mon Sep 17 00:00:00 2001 From: Carson Brown Date: Tue, 21 Aug 2012 17:07:07 -0400 Subject: [PATCH 04/55] Wipes stored object in prepare for reuse in collection view cell --- PSCollectionViewCell.m | 1 + 1 file changed, 1 insertion(+) diff --git a/PSCollectionViewCell.m b/PSCollectionViewCell.m index ff047ec..435ebf6 100644 --- a/PSCollectionViewCell.m +++ b/PSCollectionViewCell.m @@ -41,6 +41,7 @@ - (id)initWithFrame:(CGRect)frame { - (void)prepareForReuse { + self.object = nil; } - (void)fillViewWithObject:(id)object { From dd030797328190df88ab69353f6e32ebb4a6131e Mon Sep 17 00:00:00 2001 From: Adam Becevello Date: Wed, 22 Aug 2012 14:03:55 -0400 Subject: [PATCH 05/55] Properly size the header and footer views so they are the correct size at launch --- PSCollectionView.m | 2 ++ 1 file changed, 2 insertions(+) diff --git a/PSCollectionView.m b/PSCollectionView.m index 4519676..2e72ec4 100644 --- a/PSCollectionView.m +++ b/PSCollectionView.m @@ -249,6 +249,7 @@ - (void)relayoutViews { // Add headerView if it exists if (self.headerView) { + self.headerView.width = self.width; top = self.headerView.top; [self addSubview:self.headerView]; top += self.headerView.height; @@ -318,6 +319,7 @@ - (void)relayoutViews { // Add footerView if exists if (self.footerView) { + self.footerView.width = self.width; self.footerView.top = totalHeight; [self addSubview:self.footerView]; totalHeight += self.footerView.height; From 84e4dfc5934f4ef3161060c711cb278727c3778b Mon Sep 17 00:00:00 2001 From: Carson Brown Date: Wed, 22 Aug 2012 14:34:20 -0400 Subject: [PATCH 06/55] Updates readme to detail arc support --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index fdb80b9..9f8d8f5 100644 --- a/README.md +++ b/README.md @@ -32,9 +32,7 @@ It shows an example of using PSCollectionView and a subclass of PSCollectionView ARC --- -PSCollectionView, by default, is not ARC ready. - -However, there is an 'arc' branch that has been converted for use with ARC projects. +This version of PSCollectionView is ARC ready. How to use: --- From dccb7583f864eeab57e093e8016a9c7373bb32b4 Mon Sep 17 00:00:00 2001 From: Adam Becevello Date: Wed, 22 Aug 2012 14:48:09 -0400 Subject: [PATCH 07/55] Support variable height headers and footers by using sizeToFit: --- PSCollectionView.m | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/PSCollectionView.m b/PSCollectionView.m index 2e72ec4..088f320 100644 --- a/PSCollectionView.m +++ b/PSCollectionView.m @@ -250,8 +250,12 @@ - (void)relayoutViews { // Add headerView if it exists if (self.headerView) { self.headerView.width = self.width; + top = self.headerView.top; [self addSubview:self.headerView]; + + CGSize headerSize = [self.headerView sizeThatFits:CGSizeMake(self.width, CGFLOAT_MAX)]; + self.headerView.height = headerSize.height; top += self.headerView.height; } @@ -322,6 +326,9 @@ - (void)relayoutViews { self.footerView.width = self.width; self.footerView.top = totalHeight; [self addSubview:self.footerView]; + + CGSize footerSize = [self.footerView sizeThatFits:CGSizeMake(self.width, CGFLOAT_MAX)]; + self.footerView.height = footerSize.height; totalHeight += self.footerView.height; } From 8dccefe3faef6e3d6759c7aa46d2e7f157845844 Mon Sep 17 00:00:00 2001 From: Adam Becevello Date: Wed, 22 Aug 2012 15:11:25 -0400 Subject: [PATCH 08/55] Move the margin to a property so calling code can decide what value to use --- PSCollectionView.h | 1 + PSCollectionView.m | 15 +++++++++------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/PSCollectionView.h b/PSCollectionView.h index 0b1a5fb..d654422 100644 --- a/PSCollectionView.h +++ b/PSCollectionView.h @@ -36,6 +36,7 @@ @property (nonatomic, strong) UIView *emptyView; @property (nonatomic, strong) UIView *loadingView; +@property (nonatomic, assign, readwrite) CGFloat margin; @property (nonatomic, assign, readonly) CGFloat colWidth; @property (nonatomic, assign, readonly) NSInteger numCols; @property (nonatomic, assign) NSInteger numColsLandscape; diff --git a/PSCollectionView.m b/PSCollectionView.m index 088f320..db510ac 100644 --- a/PSCollectionView.m +++ b/PSCollectionView.m @@ -24,7 +24,7 @@ #import "PSCollectionView.h" #import "PSCollectionViewCell.h" -#define kMargin 8.0 +#define kDefaultMargin 8.0 static inline NSString * PSCollectionKeyForIndex(NSInteger index) { return [NSString stringWithFormat:@"%d", index]; @@ -150,6 +150,7 @@ @implementation PSCollectionView // Public @synthesize +margin = _margin, colWidth = _colWidth, numCols = _numCols, numColsLandscape = _numColsLandscape, @@ -171,6 +172,8 @@ - (id)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { self.alwaysBounceVertical = YES; + + self.margin = kDefaultMargin; self.colWidth = 0.0; self.numCols = 0; @@ -245,7 +248,7 @@ - (void)relayoutViews { NSInteger numViews = [self.collectionViewDataSource numberOfViewsInCollectionView:self]; CGFloat totalHeight = 0.0; - CGFloat top = kMargin; + CGFloat top = self.margin; // Add headerView if it exists if (self.headerView) { @@ -267,7 +270,7 @@ - (void)relayoutViews { } // Calculate index to rect mapping - self.colWidth = floorf((self.width - kMargin * (self.numCols + 1)) / self.numCols); + self.colWidth = floorf((self.width - self.margin * (self.numCols + 1)) / self.numCols); for (NSInteger i = 0; i < numViews; i++) { NSString *key = PSCollectionKeyForIndex(i); @@ -283,7 +286,7 @@ - (void)relayoutViews { } } - CGFloat left = kMargin + (col * kMargin) + (col * self.colWidth); + CGFloat left = self.margin + (col * self.margin) + (col * self.colWidth); CGFloat top = [[colOffsets objectAtIndex:col] floatValue]; CGFloat colHeight = [self.collectionViewDataSource heightForViewAtIndex:i]; if (colHeight == 0) { @@ -300,7 +303,7 @@ - (void)relayoutViews { [self.indexToRectMap setObject:NSStringFromCGRect(viewRect) forKey:key]; // Update the last height offset for this column - CGFloat test = top + colHeight + kMargin; + CGFloat test = top + colHeight + self.margin; if (test != test) { // NaN @@ -316,7 +319,7 @@ - (void)relayoutViews { // If we have an empty view, show it if (self.emptyView) { - self.emptyView.frame = CGRectMake(kMargin, top, self.width - kMargin * 2, self.height - top - kMargin); + self.emptyView.frame = CGRectMake(self.margin, top, self.width - self.margin * 2, self.height - top - self.margin); [self addSubview:self.emptyView]; } } From 2a4def9ea1eba333e8e04014c507702553957935 Mon Sep 17 00:00:00 2001 From: Carson Brown Date: Fri, 24 Aug 2012 14:54:43 -0400 Subject: [PATCH 09/55] Adds insertion to end of collection view items without reloading the whole thing cc @abecevello --- PSCollectionView.h | 5 ++ PSCollectionView.m | 157 +++++++++++++++++++++++++++++---------------- 2 files changed, 108 insertions(+), 54 deletions(-) diff --git a/PSCollectionView.h b/PSCollectionView.h index d654422..e0efd4d 100644 --- a/PSCollectionView.h +++ b/PSCollectionView.h @@ -58,6 +58,11 @@ */ - (UIView *)dequeueReusableView; +/** + Tells the collection view one more PSCollectionViewCell is added to the end + */ +- (void)appendView; + @end #pragma mark - Delegate diff --git a/PSCollectionView.m b/PSCollectionView.m index db510ac..f7c4d81 100644 --- a/PSCollectionView.m +++ b/PSCollectionView.m @@ -119,7 +119,7 @@ @interface PSCollectionView () @property (nonatomic, strong) NSMutableDictionary *visibleViews; @property (nonatomic, strong) NSMutableArray *viewKeysToRemove; @property (nonatomic, strong) NSMutableDictionary *indexToRectMap; - +@property (nonatomic, strong) NSMutableArray *colOffsets; /** Forces a relayout of the collection grid @@ -164,7 +164,8 @@ @implementation PSCollectionView reuseableViews = _reuseableViews, visibleViews = _visibleViews, viewKeysToRemove = _viewKeysToRemove, -indexToRectMap = _indexToRectMap; +indexToRectMap = _indexToRectMap, +colOffsets = _colOffsets; #pragma mark - Init/Memory @@ -227,6 +228,79 @@ - (void)layoutSubviews { } } +- (void)buildColumnOffsetsFromTop:(CGFloat) top +{ + self.colOffsets = [NSMutableArray arrayWithCapacity:self.numCols]; + for (int i = 0; i < self.numCols; i++) { + [_colOffsets addObject:[NSNumber numberWithFloat:top]]; + } +} + +- (NSInteger)findShortestColumn +{ + NSInteger col = 0; + CGFloat minHeight = [[_colOffsets objectAtIndex:col] floatValue]; + for (int i = 1; i < [_colOffsets count]; i++) { + CGFloat colHeight = [[_colOffsets objectAtIndex:i] floatValue]; + + if (colHeight < minHeight) { + col = i; + minHeight = colHeight; + } + } + return col; +} + +- (void)insertViewRectForIndex:(int)index forKey:(NSString *)key inColumn:(NSInteger)col +{ + CGFloat left = self.margin + (col * self.margin) + (col * self.colWidth); + CGFloat top = [[_colOffsets objectAtIndex:col] floatValue]; + CGFloat colHeight = [self.collectionViewDataSource heightForViewAtIndex:index]; + if (colHeight == 0) { + colHeight = self.colWidth; + } + + if (top != top) { + // NaN + } + + CGRect viewRect = CGRectMake(left, top, self.colWidth, colHeight); + + // Add to index rect map + [self.indexToRectMap setObject:NSStringFromCGRect(viewRect) forKey:key]; + + // Update the last height offset for this column + CGFloat test = top + colHeight + self.margin; + + if (test != test) { + // NaN + } + [_colOffsets replaceObjectAtIndex:col withObject:[NSNumber numberWithFloat:test]]; +} + +- (CGFloat)updateFooterViewWithTotalHeight:(CGFloat)totalHeight +{ + // Add footerView if exists + if (self.footerView) { + self.footerView.width = self.width; + self.footerView.top = totalHeight; + [self addSubview:self.footerView]; + + CGSize footerSize = [self.footerView sizeThatFits:CGSizeMake(self.width, CGFLOAT_MAX)]; + self.footerView.height = footerSize.height; + totalHeight += self.footerView.height; + } + return totalHeight; +} + +- (CGFloat)totalHeightFromColOffsetsWithTotalHeight:(CGFloat)totalHeight +{ + for (NSNumber *colHeight in _colOffsets) { + totalHeight = (totalHeight < [colHeight floatValue]) ? [colHeight floatValue] : totalHeight; + } + return totalHeight; +} + - (void)relayoutViews { self.numCols = UIInterfaceOrientationIsPortrait(self.orientation) ? self.numColsPortrait : self.numColsLandscape; @@ -264,10 +338,7 @@ - (void)relayoutViews { if (numViews > 0) { // This array determines the last height offset on a column - NSMutableArray *colOffsets = [NSMutableArray arrayWithCapacity:self.numCols]; - for (int i = 0; i < self.numCols; i++) { - [colOffsets addObject:[NSNumber numberWithFloat:top]]; - } + [self buildColumnOffsetsFromTop:top]; // Calculate index to rect mapping self.colWidth = floorf((self.width - self.margin * (self.numCols + 1)) / self.numCols); @@ -275,45 +346,11 @@ - (void)relayoutViews { NSString *key = PSCollectionKeyForIndex(i); // Find the shortest column - NSInteger col = 0; - CGFloat minHeight = [[colOffsets objectAtIndex:col] floatValue]; - for (int i = 1; i < [colOffsets count]; i++) { - CGFloat colHeight = [[colOffsets objectAtIndex:i] floatValue]; - - if (colHeight < minHeight) { - col = i; - minHeight = colHeight; - } - } - - CGFloat left = self.margin + (col * self.margin) + (col * self.colWidth); - CGFloat top = [[colOffsets objectAtIndex:col] floatValue]; - CGFloat colHeight = [self.collectionViewDataSource heightForViewAtIndex:i]; - if (colHeight == 0) { - colHeight = self.colWidth; - } - - if (top != top) { - // NaN - } - - CGRect viewRect = CGRectMake(left, top, self.colWidth, colHeight); - - // Add to index rect map - [self.indexToRectMap setObject:NSStringFromCGRect(viewRect) forKey:key]; - - // Update the last height offset for this column - CGFloat test = top + colHeight + self.margin; - - if (test != test) { - // NaN - } - [colOffsets replaceObjectAtIndex:col withObject:[NSNumber numberWithFloat:test]]; - } - - for (NSNumber *colHeight in colOffsets) { - totalHeight = (totalHeight < [colHeight floatValue]) ? [colHeight floatValue] : totalHeight; + NSInteger col = [self findShortestColumn]; + [self insertViewRectForIndex:i forKey:key inColumn:col]; } + + totalHeight = [self totalHeightFromColOffsetsWithTotalHeight:(CGFloat)totalHeight]; } else { totalHeight = self.height; @@ -324,16 +361,7 @@ - (void)relayoutViews { } } - // Add footerView if exists - if (self.footerView) { - self.footerView.width = self.width; - self.footerView.top = totalHeight; - [self addSubview:self.footerView]; - - CGSize footerSize = [self.footerView sizeThatFits:CGSizeMake(self.width, CGFLOAT_MAX)]; - self.footerView.height = footerSize.height; - totalHeight += self.footerView.height; - } + totalHeight = [self updateFooterViewWithTotalHeight:totalHeight]; self.contentSize = CGSizeMake(self.width, totalHeight); @@ -411,6 +439,27 @@ - (void)removeAndAddCellsIfNecessary { } } +- (void)appendView +{ + NSInteger numViews = [self.collectionViewDataSource numberOfViewsInCollectionView:self]; + if ([self.indexToRectMap count] == 0 || numViews == 1) { + //just build via a reload + [self reloadData]; + } else { + NSString *key = PSCollectionKeyForIndex(numViews-1); + + // Find the shortest column + NSInteger col = [self findShortestColumn]; + [self insertViewRectForIndex:numViews-1 forKey:key inColumn:col]; + CGFloat totalHeight = [self totalHeightFromColOffsetsWithTotalHeight:0.0f]; + + totalHeight = [self updateFooterViewWithTotalHeight:totalHeight]; + + self.contentSize = CGSizeMake(self.width, totalHeight); + [self removeAndAddCellsIfNecessary]; + } +} + #pragma mark - Reusing Views - (PSCollectionViewCell *)dequeueReusableView { From a8a5685e9d4edf3bdcabbd0dd5835adc1da1468b Mon Sep 17 00:00:00 2001 From: Carson Brown Date: Tue, 28 Aug 2012 13:53:34 -0400 Subject: [PATCH 10/55] Converts stored indexes to be NSNumbers Using NSStrings (via NSStringWithFormat) was very expensive! --- PSCollectionView.m | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/PSCollectionView.m b/PSCollectionView.m index f7c4d81..8c78ac9 100644 --- a/PSCollectionView.m +++ b/PSCollectionView.m @@ -26,8 +26,9 @@ #define kDefaultMargin 8.0 -static inline NSString * PSCollectionKeyForIndex(NSInteger index) { - return [NSString stringWithFormat:@"%d", index]; +static inline NSNumber * PSCollectionKeyForIndex(NSInteger index) { + //return [NSString stringWithFormat:@"%d", index]; + return [NSNumber numberWithInteger:index]; } static inline NSInteger PSCollectionIndexForKey(NSString *key) { @@ -343,7 +344,7 @@ - (void)relayoutViews { // Calculate index to rect mapping self.colWidth = floorf((self.width - self.margin * (self.numCols + 1)) / self.numCols); for (NSInteger i = 0; i < numViews; i++) { - NSString *key = PSCollectionKeyForIndex(i); + NSNumber *key = PSCollectionKeyForIndex(i); // Find the shortest column NSInteger col = [self findShortestColumn]; @@ -416,8 +417,8 @@ - (void)removeAndAddCellsIfNecessary { // Add views for (NSInteger i = topIndex; i < bottomIndex; i++) { - NSString *key = PSCollectionKeyForIndex(i); CGRect rect = CGRectFromString([self.indexToRectMap objectForKey:key]); + NSNumber *key = PSCollectionKeyForIndex(i); // If view is within visible rect and is not already shown if (![self.visibleViews objectForKey:key] && CGRectIntersectsRect(visibleRect, rect)) { @@ -446,7 +447,7 @@ - (void)appendView //just build via a reload [self reloadData]; } else { - NSString *key = PSCollectionKeyForIndex(numViews-1); + NSNumber *key = PSCollectionKeyForIndex(numViews-1); // Find the shortest column NSInteger col = [self findShortestColumn]; From 66c9ec3f5a5b373442587155cfd63212bd450cd9 Mon Sep 17 00:00:00 2001 From: Carson Brown Date: Tue, 28 Aug 2012 13:54:16 -0400 Subject: [PATCH 11/55] Refactors insert method to handle new key type Staying generic, but this is technically only NSNumbers --- PSCollectionView.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PSCollectionView.m b/PSCollectionView.m index 8c78ac9..cce3d33 100644 --- a/PSCollectionView.m +++ b/PSCollectionView.m @@ -252,7 +252,7 @@ - (NSInteger)findShortestColumn return col; } -- (void)insertViewRectForIndex:(int)index forKey:(NSString *)key inColumn:(NSInteger)col +- (void)insertViewRectForIndex:(int)index forKey:(id )key inColumn:(NSInteger)col { CGFloat left = self.margin + (col * self.margin) + (col * self.colWidth); CGFloat top = [[_colOffsets objectAtIndex:col] floatValue]; From 0915564b91cfdba64624a60bcd3ef03f2ff03621 Mon Sep 17 00:00:00 2001 From: Carson Brown Date: Tue, 28 Aug 2012 13:55:04 -0400 Subject: [PATCH 12/55] Changes wrapping of CGRects to use NSValue Converting to and from NSStrings was expensive --- PSCollectionView.m | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/PSCollectionView.m b/PSCollectionView.m index cce3d33..a805af5 100644 --- a/PSCollectionView.m +++ b/PSCollectionView.m @@ -268,7 +268,7 @@ - (void)insertViewRectForIndex:(int)index forKey:(id )key inColumn:(N CGRect viewRect = CGRectMake(left, top, self.colWidth, colHeight); // Add to index rect map - [self.indexToRectMap setObject:NSStringFromCGRect(viewRect) forKey:key]; + [self.indexToRectMap setObject:[NSValue valueWithCGRect:viewRect] forKey:key]; // Update the last height offset for this column CGFloat test = top + colHeight + self.margin; @@ -417,14 +417,14 @@ - (void)removeAndAddCellsIfNecessary { // Add views for (NSInteger i = topIndex; i < bottomIndex; i++) { - CGRect rect = CGRectFromString([self.indexToRectMap objectForKey:key]); NSNumber *key = PSCollectionKeyForIndex(i); + CGRect rect = [[self.indexToRectMap objectForKey:key] CGRectValue]; // If view is within visible rect and is not already shown if (![self.visibleViews objectForKey:key] && CGRectIntersectsRect(visibleRect, rect)) { // Only add views if not visible PSCollectionViewCell *newView = [self.collectionViewDataSource collectionView:self viewAtIndex:i]; - newView.frame = CGRectFromString([self.indexToRectMap objectForKey:key]); + newView.frame = [[self.indexToRectMap objectForKey:key] CGRectValue]; [self addSubview:newView]; // Setup gesture recognizer @@ -484,9 +484,9 @@ - (void)enqueueReusableView:(PSCollectionViewCell *)view { #pragma mark - Gesture Recognizer -- (void)didSelectView:(UITapGestureRecognizer *)gestureRecognizer { - NSString *rectString = NSStringFromCGRect(gestureRecognizer.view.frame); - NSArray *matchingKeys = [self.indexToRectMap allKeysForObject:rectString]; +- (void)didSelectView:(UITapGestureRecognizer *)gestureRecognizer { + NSValue *rectValue = [NSValue valueWithCGRect:gestureRecognizer.view.frame]; + NSArray *matchingKeys = [self.indexToRectMap allKeysForObject:rectValue]; NSString *key = [matchingKeys lastObject]; if ([gestureRecognizer.view isMemberOfClass:[[self.visibleViews objectForKey:key] class]]) { if (self.collectionViewDelegate && [self.collectionViewDelegate respondsToSelector:@selector(collectionView:didSelectView:atIndex:)]) { @@ -499,8 +499,8 @@ - (void)didSelectView:(UITapGestureRecognizer *)gestureRecognizer { - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch { if (![gestureRecognizer isMemberOfClass:[PSCollectionViewTapGestureRecognizer class]]) return YES; - NSString *rectString = NSStringFromCGRect(gestureRecognizer.view.frame); - NSArray *matchingKeys = [self.indexToRectMap allKeysForObject:rectString]; + NSValue *rectValue = [NSValue valueWithCGRect:gestureRecognizer.view.frame]; + NSArray *matchingKeys = [self.indexToRectMap allKeysForObject:rectValue]; NSString *key = [matchingKeys lastObject]; if ([touch.view isMemberOfClass:[[self.visibleViews objectForKey:key] class]]) { From 9644ab9fd6cf3c9498b0b608a2ad0e80fb813e41 Mon Sep 17 00:00:00 2001 From: Carson Brown Date: Tue, 28 Aug 2012 14:06:38 -0400 Subject: [PATCH 13/55] Removes commented out line of code --- PSCollectionView.m | 1 - 1 file changed, 1 deletion(-) diff --git a/PSCollectionView.m b/PSCollectionView.m index a805af5..7e77c49 100644 --- a/PSCollectionView.m +++ b/PSCollectionView.m @@ -27,7 +27,6 @@ #define kDefaultMargin 8.0 static inline NSNumber * PSCollectionKeyForIndex(NSInteger index) { - //return [NSString stringWithFormat:@"%d", index]; return [NSNumber numberWithInteger:index]; } From 9c6e59c65a03801113cc76f31bcdd747704bdc04 Mon Sep 17 00:00:00 2001 From: Carson Brown Date: Tue, 28 Aug 2012 16:00:06 -0400 Subject: [PATCH 14/55] Fades in newly appearing items in collection Only fades them in once, though. You need to `reloadData` in order to reset which items are faded in. --- PSCollectionView.m | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/PSCollectionView.m b/PSCollectionView.m index 7e77c49..17b0dd0 100644 --- a/PSCollectionView.m +++ b/PSCollectionView.m @@ -25,6 +25,7 @@ #import "PSCollectionViewCell.h" #define kDefaultMargin 8.0 +#define kAnimationDuration 0.3f static inline NSNumber * PSCollectionKeyForIndex(NSInteger index) { return [NSNumber numberWithInteger:index]; @@ -120,6 +121,7 @@ @interface PSCollectionView () @property (nonatomic, strong) NSMutableArray *viewKeysToRemove; @property (nonatomic, strong) NSMutableDictionary *indexToRectMap; @property (nonatomic, strong) NSMutableArray *colOffsets; +@property (nonatomic, strong) NSMutableIndexSet *loadedIndices; /** Forces a relayout of the collection grid @@ -139,7 +141,9 @@ - (void)removeAndAddCellsIfNecessary; @end -@implementation PSCollectionView +@implementation PSCollectionView { + BOOL _resetLoadedIndices; +} // Public Views @synthesize @@ -165,7 +169,8 @@ @implementation PSCollectionView visibleViews = _visibleViews, viewKeysToRemove = _viewKeysToRemove, indexToRectMap = _indexToRectMap, -colOffsets = _colOffsets; +colOffsets = _colOffsets, +loadedIndices = _loadedIndices; #pragma mark - Init/Memory @@ -186,6 +191,7 @@ - (id)initWithFrame:(CGRect)frame { self.visibleViews = [NSMutableDictionary dictionary]; self.viewKeysToRemove = [NSMutableArray array]; self.indexToRectMap = [NSMutableDictionary dictionary]; + self.loadedIndices = [NSMutableIndexSet indexSet]; } return self; } @@ -211,6 +217,7 @@ - (void)setLoadingView:(UIView *)loadingView { #pragma mark - DataSource - (void)reloadData { + _resetLoadedIndices = YES; [self relayoutViews]; } @@ -312,7 +319,11 @@ - (void)relayoutViews { [self.visibleViews removeAllObjects]; [self.viewKeysToRemove removeAllObjects]; [self.indexToRectMap removeAllObjects]; - + if (_resetLoadedIndices) { + self.loadedIndices = [NSMutableIndexSet indexSet]; + _resetLoadedIndices = NO; + } + if (self.emptyView) { [self.emptyView removeFromSuperview]; } @@ -424,8 +435,17 @@ - (void)removeAndAddCellsIfNecessary { // Only add views if not visible PSCollectionViewCell *newView = [self.collectionViewDataSource collectionView:self viewAtIndex:i]; newView.frame = [[self.indexToRectMap objectForKey:key] CGRectValue]; - [self addSubview:newView]; - + if ([self.loadedIndices containsIndex:i]) { + [self addSubview:newView]; + } else { //animate it in, add it to the set + [self.loadedIndices addIndex:i]; + newView.alpha = 0.0f; + [self addSubview:newView]; + [UIView animateWithDuration:kAnimationDuration animations:^{ + newView.alpha = 1.0f; + }]; + } + // Setup gesture recognizer if ([newView.gestureRecognizers count] == 0) { PSCollectionViewTapGestureRecognizer *gr = [[PSCollectionViewTapGestureRecognizer alloc] initWithTarget:self action:@selector(didSelectView:)]; @@ -477,6 +497,7 @@ - (void)enqueueReusableView:(PSCollectionViewCell *)view { [view performSelector:@selector(prepareForReuse)]; } view.frame = CGRectZero; + view.alpha = 1.0f; [self.reuseableViews addObject:view]; [view removeFromSuperview]; } From 9cc27e6b2aede07c1e4f0dc4ba3f1edaae5276c9 Mon Sep 17 00:00:00 2001 From: Carson Brown Date: Wed, 29 Aug 2012 16:04:41 -0400 Subject: [PATCH 15/55] Adds boolean to disable/enable introduction fade on items desk checked by @abecevello --- PSCollectionView.h | 1 + PSCollectionView.m | 12 ++++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/PSCollectionView.h b/PSCollectionView.h index e0efd4d..fed2f71 100644 --- a/PSCollectionView.h +++ b/PSCollectionView.h @@ -41,6 +41,7 @@ @property (nonatomic, assign, readonly) NSInteger numCols; @property (nonatomic, assign) NSInteger numColsLandscape; @property (nonatomic, assign) NSInteger numColsPortrait; +@property (nonatomic, assign) BOOL animateFirstCellAppearance; @property (nonatomic, unsafe_unretained) id collectionViewDelegate; @property (nonatomic, unsafe_unretained) id collectionViewDataSource; diff --git a/PSCollectionView.m b/PSCollectionView.m index 17b0dd0..8b2b7a3 100644 --- a/PSCollectionView.m +++ b/PSCollectionView.m @@ -159,6 +159,7 @@ @implementation PSCollectionView { numCols = _numCols, numColsLandscape = _numColsLandscape, numColsPortrait = _numColsPortrait, +animateFirstCellAppearance = _animateFirstCellAppearance, collectionViewDelegate = _collectionViewDelegate, collectionViewDataSource = _collectionViewDataSource; @@ -192,6 +193,7 @@ - (id)initWithFrame:(CGRect)frame { self.viewKeysToRemove = [NSMutableArray array]; self.indexToRectMap = [NSMutableDictionary dictionary]; self.loadedIndices = [NSMutableIndexSet indexSet]; + self.animateFirstCellAppearance = YES; } return self; } @@ -439,11 +441,13 @@ - (void)removeAndAddCellsIfNecessary { [self addSubview:newView]; } else { //animate it in, add it to the set [self.loadedIndices addIndex:i]; - newView.alpha = 0.0f; [self addSubview:newView]; - [UIView animateWithDuration:kAnimationDuration animations:^{ - newView.alpha = 1.0f; - }]; + if (self.animateFirstCellAppearance) { + newView.alpha = 0.0f; + [UIView animateWithDuration:kAnimationDuration animations:^{ + newView.alpha = 1.0f; + }]; + } } // Setup gesture recognizer From 4fe53aa48ca74d5202558d989da51922e956474a Mon Sep 17 00:00:00 2001 From: Adam Becevello Date: Tue, 4 Sep 2012 13:28:20 -0400 Subject: [PATCH 16/55] Support header views that change height --- PSCollectionView.m | 42 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/PSCollectionView.m b/PSCollectionView.m index 8b2b7a3..b51aac7 100644 --- a/PSCollectionView.m +++ b/PSCollectionView.m @@ -123,6 +123,8 @@ @interface PSCollectionView () @property (nonatomic, strong) NSMutableArray *colOffsets; @property (nonatomic, strong) NSMutableIndexSet *loadedIndices; +@property (nonatomic, assign, readwrite) CGFloat headerViewHeight; + /** Forces a relayout of the collection grid */ @@ -171,7 +173,8 @@ @implementation PSCollectionView { viewKeysToRemove = _viewKeysToRemove, indexToRectMap = _indexToRectMap, colOffsets = _colOffsets, -loadedIndices = _loadedIndices; +loadedIndices = _loadedIndices, +headerViewHeight = _headerViewHeight; #pragma mark - Init/Memory @@ -194,6 +197,7 @@ - (id)initWithFrame:(CGRect)frame { self.indexToRectMap = [NSMutableDictionary dictionary]; self.loadedIndices = [NSMutableIndexSet indexSet]; self.animateFirstCellAppearance = YES; + self.headerViewHeight = 0.0f; } return self; } @@ -232,9 +236,38 @@ - (void)layoutSubviews { if (self.orientation != orientation) { self.orientation = orientation; [self relayoutViews]; - } else { - [self removeAndAddCellsIfNecessary]; - } + } else { + //determine if the header has changed height + CGSize headerSize = [self.headerView sizeThatFits:CGSizeMake(self.width, CGFLOAT_MAX)]; + if (self.headerViewHeight != headerSize.height) { + //need to adjust all the cells and column heights to reflect the new header height + CGFloat delta = headerSize.height - self.headerViewHeight; + + self.headerView.height = headerSize.height; + self.headerViewHeight = headerSize.height; + + [self adjustYOffsetsWithDelta:delta]; + } else { + [self removeAndAddCellsIfNecessary]; + } + } +} + +- (void)adjustYOffsetsWithDelta:(CGFloat)delta +{ + //adjust the column heights + for (int i = 0; i < self.numCols; i++) { + CGFloat colHeight = [[_colOffsets objectAtIndex:i] floatValue]; + NSNumber *newOffset = [NSNumber numberWithFloat:colHeight + delta]; + [_colOffsets replaceObjectAtIndex:i withObject:newOffset]; + } + + //adjust all the cell positions + NSArray *visibleViewValues = _visibleViews.allValues; + for (UIView *visibleView in visibleViewValues) { + CGRect newFrame = CGRectOffset(visibleView.frame, 0, delta); + visibleView.frame = newFrame; + } } - (void)buildColumnOffsetsFromTop:(CGFloat) top @@ -346,6 +379,7 @@ - (void)relayoutViews { CGSize headerSize = [self.headerView sizeThatFits:CGSizeMake(self.width, CGFLOAT_MAX)]; self.headerView.height = headerSize.height; + self.headerViewHeight = headerSize.height; top += self.headerView.height; } From 7a8c7b39004d3036d128e4aa8ca8fdc64c64d743 Mon Sep 17 00:00:00 2001 From: Adam Becevello Date: Tue, 4 Sep 2012 14:28:33 -0400 Subject: [PATCH 17/55] Update with fixes for review comments --- PSCollectionView.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PSCollectionView.m b/PSCollectionView.m index b51aac7..f886cd2 100644 --- a/PSCollectionView.m +++ b/PSCollectionView.m @@ -247,9 +247,9 @@ - (void)layoutSubviews { self.headerViewHeight = headerSize.height; [self adjustYOffsetsWithDelta:delta]; - } else { - [self removeAndAddCellsIfNecessary]; } + + [self removeAndAddCellsIfNecessary]; } } From 40be69e0b541e108a57f07e846d87c44b13cd800 Mon Sep 17 00:00:00 2001 From: Carson Brown Date: Wed, 5 Sep 2012 15:13:52 -0400 Subject: [PATCH 18/55] Enable user interaction while views are faded in Previously you couldn't! :-O --- PSCollectionView.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PSCollectionView.m b/PSCollectionView.m index f886cd2..2fee6c9 100644 --- a/PSCollectionView.m +++ b/PSCollectionView.m @@ -478,9 +478,9 @@ - (void)removeAndAddCellsIfNecessary { [self addSubview:newView]; if (self.animateFirstCellAppearance) { newView.alpha = 0.0f; - [UIView animateWithDuration:kAnimationDuration animations:^{ + [UIView animateWithDuration:kAnimationDuration delay:0.0f options:UIViewAnimationOptionAllowUserInteraction animations:^{ newView.alpha = 1.0f; - }]; + } completion:nil]; } } From 531e4fa5f457009b73fb9a97c8572a19293d87d4 Mon Sep 17 00:00:00 2001 From: Adam Becevello Date: Tue, 18 Sep 2012 16:06:34 -0400 Subject: [PATCH 19/55] Override all view setters so the old views are properly removed --- PSCollectionView.m | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/PSCollectionView.m b/PSCollectionView.m index 2fee6c9..d01d4e0 100644 --- a/PSCollectionView.m +++ b/PSCollectionView.m @@ -212,12 +212,47 @@ - (void)dealloc { #pragma mark - Setters - (void)setLoadingView:(UIView *)loadingView { - if (_loadingView && [_loadingView respondsToSelector:@selector(removeFromSuperview)]) { + if (_loadingView) { [_loadingView removeFromSuperview]; } _loadingView = loadingView; [self addSubview:_loadingView]; + + [self relayoutViews]; +} + +- (void)setEmptyView:(UIView *)emptyView { + if (_emptyView) { + [_emptyView removeFromSuperview]; + } + _emptyView = emptyView; + + [self addSubview:_emptyView]; + + [self relayoutViews]; +} + +- (void)setHeaderView:(UIView *)headerView { + if (_headerView) { + [_headerView removeFromSuperview]; + } + _headerView = headerView; + + [self addSubview:_headerView]; + + [self relayoutViews]; +} + +- (void)setFooterView:(UIView *)footerView { + if (_footerView) { + [_footerView removeFromSuperview]; + } + _footerView = footerView; + + [self addSubview:_footerView]; + + [self relayoutViews]; } #pragma mark - DataSource From b3eefedb348518104dab156f4175c54955846253 Mon Sep 17 00:00:00 2001 From: Carson Brown Date: Thu, 20 Sep 2012 13:02:17 -0400 Subject: [PATCH 20/55] Removes expensive sort in exchange for an O(n) single pass --- PSCollectionView.m | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/PSCollectionView.m b/PSCollectionView.m index 2fee6c9..7fce0cd 100644 --- a/PSCollectionView.m +++ b/PSCollectionView.m @@ -444,18 +444,20 @@ - (void)removeAndAddCellsIfNecessary { topIndex = 0; bottomIndex = numViews; } else { - NSArray *sortedKeys = [[self.visibleViews allKeys] sortedArrayUsingComparator:^(id obj1, id obj2) { - if ([obj1 integerValue] < [obj2 integerValue]) { - return (NSComparisonResult)NSOrderedAscending; - } else if ([obj1 integerValue] > [obj2 integerValue]) { - return (NSComparisonResult)NSOrderedDescending; - } else { - return (NSComparisonResult)NSOrderedSame; - } - }]; - topIndex = [[sortedKeys objectAtIndex:0] integerValue]; - bottomIndex = [[sortedKeys lastObject] integerValue]; - + // need the highest and lowest values, so instead of an expensive sort, just iterate finding the high/low + NSArray *allKeys = [self.visibleViews allKeys]; + topIndex = [[allKeys objectAtIndex:0] integerValue]; + bottomIndex = [[allKeys objectAtIndex:0] integerValue]; + [allKeys enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { + NSInteger value = [obj integerValue]; + if (value < topIndex) { + topIndex = value; + } + if (value > bottomIndex) { + bottomIndex = value; + } + }]; + topIndex = MAX(0, topIndex - (bufferViewFactor * self.numCols)); bottomIndex = MIN(numViews, bottomIndex + (bufferViewFactor * self.numCols)); } From e82fb106788be036727d4035e6a81bb1b85dbc4e Mon Sep 17 00:00:00 2001 From: Carson Brown Date: Wed, 10 Oct 2012 16:34:36 -0400 Subject: [PATCH 21/55] Removes shifting yOffsets for just relaying out views This is due to a bug where the visible rows are fixed, but are back to their original offsets after a scroll down the grid and back. --- PSCollectionView.m | 23 ++--------------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/PSCollectionView.m b/PSCollectionView.m index e8fb5f6..d35298f 100644 --- a/PSCollectionView.m +++ b/PSCollectionView.m @@ -275,36 +275,17 @@ - (void)layoutSubviews { //determine if the header has changed height CGSize headerSize = [self.headerView sizeThatFits:CGSizeMake(self.width, CGFLOAT_MAX)]; if (self.headerViewHeight != headerSize.height) { - //need to adjust all the cells and column heights to reflect the new header height - CGFloat delta = headerSize.height - self.headerViewHeight; - self.headerView.height = headerSize.height; self.headerViewHeight = headerSize.height; - [self adjustYOffsetsWithDelta:delta]; + //need to adjust all the cells and column heights to reflect the new header height + [self relayoutViews]; } [self removeAndAddCellsIfNecessary]; } } -- (void)adjustYOffsetsWithDelta:(CGFloat)delta -{ - //adjust the column heights - for (int i = 0; i < self.numCols; i++) { - CGFloat colHeight = [[_colOffsets objectAtIndex:i] floatValue]; - NSNumber *newOffset = [NSNumber numberWithFloat:colHeight + delta]; - [_colOffsets replaceObjectAtIndex:i withObject:newOffset]; - } - - //adjust all the cell positions - NSArray *visibleViewValues = _visibleViews.allValues; - for (UIView *visibleView in visibleViewValues) { - CGRect newFrame = CGRectOffset(visibleView.frame, 0, delta); - visibleView.frame = newFrame; - } -} - - (void)buildColumnOffsetsFromTop:(CGFloat) top { self.colOffsets = [NSMutableArray arrayWithCapacity:self.numCols]; From 0de635a66880974a8f5a15bd0953fc2c212fa026 Mon Sep 17 00:00:00 2001 From: Adam Becevello Date: Tue, 6 Nov 2012 14:13:55 -0500 Subject: [PATCH 22/55] Ensure the margin is present after header views --- PSCollectionView.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PSCollectionView.m b/PSCollectionView.m index d35298f..87ac3d6 100644 --- a/PSCollectionView.m +++ b/PSCollectionView.m @@ -396,7 +396,7 @@ - (void)relayoutViews { CGSize headerSize = [self.headerView sizeThatFits:CGSizeMake(self.width, CGFLOAT_MAX)]; self.headerView.height = headerSize.height; self.headerViewHeight = headerSize.height; - top += self.headerView.height; + top += self.headerView.height + self.margin; } if (numViews > 0) { From 6f827bf9ea058a71c047555a9d1db5ddf42b8e55 Mon Sep 17 00:00:00 2001 From: Joshua Gosse Date: Thu, 8 Nov 2012 10:39:03 -0500 Subject: [PATCH 23/55] Now takes into consideration the height of the headerView if no gridItems are present. Previously the height of the header was never added to the total height, and therefore did not update the contentSize of the PSCollectionView --- PSCollectionView.m | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/PSCollectionView.m b/PSCollectionView.m index 87ac3d6..3210fe1 100644 --- a/PSCollectionView.m +++ b/PSCollectionView.m @@ -421,7 +421,9 @@ - (void)relayoutViews { if (self.emptyView) { self.emptyView.frame = CGRectMake(self.margin, top, self.width - self.margin * 2, self.height - top - self.margin); [self addSubview:self.emptyView]; - } + } else if (self.headerView) { + totalHeight = top; + } } totalHeight = [self updateFooterViewWithTotalHeight:totalHeight]; From 8034f485745f6e052807d2c723d9c0bcd8ae8729 Mon Sep 17 00:00:00 2001 From: Joshua Tessier Date: Thu, 15 Nov 2012 13:44:39 -0500 Subject: [PATCH 24/55] Removes a small bit of overhead from using NSNumbers 1) Adds PSCollectionViewKey and uses it instead of NSNumber. NSNumber was slower on comparisons (we can do a simple integer compare) 2) Fixes casting issues (keys were being cast to Strings) --- PSCollectionView.m | 66 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 55 insertions(+), 11 deletions(-) diff --git a/PSCollectionView.m b/PSCollectionView.m index 3210fe1..b1afa64 100644 --- a/PSCollectionView.m +++ b/PSCollectionView.m @@ -27,12 +27,56 @@ #define kDefaultMargin 8.0 #define kAnimationDuration 0.3f -static inline NSNumber * PSCollectionKeyForIndex(NSInteger index) { - return [NSNumber numberWithInteger:index]; +@interface PSCollectionViewKey : NSObject + +- (instancetype)initWithInteger:(NSInteger)integer; ++ (PSCollectionViewKey*)keyWithInteger:(NSInteger)integer; + +@property (nonatomic, assign) NSInteger key; + +@end + +@implementation PSCollectionViewKey + +- (id)copyWithZone:(NSZone *)zone +{ + PSCollectionViewKey *newKey = [[self class] allocWithZone:zone]; + newKey.key = self.key; + return newKey; } -static inline NSInteger PSCollectionIndexForKey(NSString *key) { - return [key integerValue]; +- (instancetype)initWithInteger:(NSInteger)integer +{ + self = [super init]; + if (self) { + _key = integer; + } + return self; +} + ++ (PSCollectionViewKey*)keyWithInteger:(NSInteger)integer +{ + return [[PSCollectionViewKey alloc] initWithInteger:integer]; +} + +- (NSUInteger)hash +{ + return _key; +} + +- (BOOL)isEqual:(id)object +{ + return _key == ((PSCollectionViewKey*)object).key; +} + +@end + +static inline PSCollectionViewKey * PSCollectionKeyForIndex(NSInteger index) { + return [PSCollectionViewKey keyWithInteger:index]; +} + +static inline NSInteger PSCollectionIndexForKey(PSCollectionViewKey *key) { + return key.key; } #pragma mark - UIView Category @@ -406,7 +450,7 @@ - (void)relayoutViews { // Calculate index to rect mapping self.colWidth = floorf((self.width - self.margin * (self.numCols + 1)) / self.numCols); for (NSInteger i = 0; i < numViews; i++) { - NSNumber *key = PSCollectionKeyForIndex(i); + PSCollectionViewKey *key = PSCollectionKeyForIndex(i); // Find the shortest column NSInteger col = [self findShortestColumn]; @@ -464,10 +508,10 @@ - (void)removeAndAddCellsIfNecessary { } else { // need the highest and lowest values, so instead of an expensive sort, just iterate finding the high/low NSArray *allKeys = [self.visibleViews allKeys]; - topIndex = [[allKeys objectAtIndex:0] integerValue]; - bottomIndex = [[allKeys objectAtIndex:0] integerValue]; + topIndex = [(PSCollectionViewKey*)[allKeys objectAtIndex:0] key]; + bottomIndex = [(PSCollectionViewKey*)[allKeys objectAtIndex:0] key]; [allKeys enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { - NSInteger value = [obj integerValue]; + NSInteger value = [(PSCollectionViewKey*)obj key]; if (value < topIndex) { topIndex = value; } @@ -483,7 +527,7 @@ - (void)removeAndAddCellsIfNecessary { // Add views for (NSInteger i = topIndex; i < bottomIndex; i++) { - NSNumber *key = PSCollectionKeyForIndex(i); + PSCollectionViewKey *key = PSCollectionKeyForIndex(i); CGRect rect = [[self.indexToRectMap objectForKey:key] CGRectValue]; // If view is within visible rect and is not already shown @@ -524,7 +568,7 @@ - (void)appendView //just build via a reload [self reloadData]; } else { - NSNumber *key = PSCollectionKeyForIndex(numViews-1); + PSCollectionViewKey *key = PSCollectionKeyForIndex(numViews-1); // Find the shortest column NSInteger col = [self findShortestColumn]; @@ -565,7 +609,7 @@ - (void)enqueueReusableView:(PSCollectionViewCell *)view { - (void)didSelectView:(UITapGestureRecognizer *)gestureRecognizer { NSValue *rectValue = [NSValue valueWithCGRect:gestureRecognizer.view.frame]; NSArray *matchingKeys = [self.indexToRectMap allKeysForObject:rectValue]; - NSString *key = [matchingKeys lastObject]; + PSCollectionViewKey *key = [matchingKeys lastObject]; if ([gestureRecognizer.view isMemberOfClass:[[self.visibleViews objectForKey:key] class]]) { if (self.collectionViewDelegate && [self.collectionViewDelegate respondsToSelector:@selector(collectionView:didSelectView:atIndex:)]) { NSInteger matchingIndex = PSCollectionIndexForKey([matchingKeys lastObject]); From aeb2b0b8b6444f4aaefcf7c30bf675457a92840a Mon Sep 17 00:00:00 2001 From: Adam Becevello Date: Mon, 26 Nov 2012 13:37:54 -0500 Subject: [PATCH 25/55] Detect when the footer needs to cause a relayout due to changing size --- PSCollectionView.m | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/PSCollectionView.m b/PSCollectionView.m index b1afa64..915aceb 100644 --- a/PSCollectionView.m +++ b/PSCollectionView.m @@ -168,6 +168,7 @@ @interface PSCollectionView () @property (nonatomic, strong) NSMutableIndexSet *loadedIndices; @property (nonatomic, assign, readwrite) CGFloat headerViewHeight; +@property (nonatomic, assign, readwrite) CGFloat footerViewHeight; /** Forces a relayout of the collection grid @@ -218,7 +219,8 @@ @implementation PSCollectionView { indexToRectMap = _indexToRectMap, colOffsets = _colOffsets, loadedIndices = _loadedIndices, -headerViewHeight = _headerViewHeight; +headerViewHeight = _headerViewHeight, +footerViewHeight = _footerViewHeight; #pragma mark - Init/Memory @@ -242,6 +244,7 @@ - (id)initWithFrame:(CGRect)frame { self.loadedIndices = [NSMutableIndexSet indexSet]; self.animateFirstCellAppearance = YES; self.headerViewHeight = 0.0f; + self.footerViewHeight = 0.0f; } return self; } @@ -326,6 +329,13 @@ - (void)layoutSubviews { [self relayoutViews]; } + CGSize footerSize = [self.footerView sizeThatFits:CGSizeMake(self.width, CGFLOAT_MAX)]; + if (self.footerViewHeight != footerSize.height) { + self.footerViewHeight = footerSize.height; + + [self relayoutViews]; + } + [self removeAndAddCellsIfNecessary]; } } From 3c01fe3e451c2f39c35a6b30662f31fb77fb7fea Mon Sep 17 00:00:00 2001 From: Adam Becevello Date: Mon, 26 Nov 2012 13:44:46 -0500 Subject: [PATCH 26/55] Revert "Detect when the footer needs to cause a relayout due to changing size" This reverts commit aeb2b0b8b6444f4aaefcf7c30bf675457a92840a. --- PSCollectionView.m | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/PSCollectionView.m b/PSCollectionView.m index 915aceb..b1afa64 100644 --- a/PSCollectionView.m +++ b/PSCollectionView.m @@ -168,7 +168,6 @@ @interface PSCollectionView () @property (nonatomic, strong) NSMutableIndexSet *loadedIndices; @property (nonatomic, assign, readwrite) CGFloat headerViewHeight; -@property (nonatomic, assign, readwrite) CGFloat footerViewHeight; /** Forces a relayout of the collection grid @@ -219,8 +218,7 @@ @implementation PSCollectionView { indexToRectMap = _indexToRectMap, colOffsets = _colOffsets, loadedIndices = _loadedIndices, -headerViewHeight = _headerViewHeight, -footerViewHeight = _footerViewHeight; +headerViewHeight = _headerViewHeight; #pragma mark - Init/Memory @@ -244,7 +242,6 @@ - (id)initWithFrame:(CGRect)frame { self.loadedIndices = [NSMutableIndexSet indexSet]; self.animateFirstCellAppearance = YES; self.headerViewHeight = 0.0f; - self.footerViewHeight = 0.0f; } return self; } @@ -329,13 +326,6 @@ - (void)layoutSubviews { [self relayoutViews]; } - CGSize footerSize = [self.footerView sizeThatFits:CGSizeMake(self.width, CGFLOAT_MAX)]; - if (self.footerViewHeight != footerSize.height) { - self.footerViewHeight = footerSize.height; - - [self relayoutViews]; - } - [self removeAndAddCellsIfNecessary]; } } From 5e147b2d04a6ea16dfe7d41d953c558052c8d9f2 Mon Sep 17 00:00:00 2001 From: Adam Becevello Date: Tue, 27 Nov 2012 09:53:02 -0500 Subject: [PATCH 27/55] When we don't have any cells to display, don't force the view to take up the entire height unless we have an empty view --- PSCollectionView.m | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/PSCollectionView.m b/PSCollectionView.m index b1afa64..febe70c 100644 --- a/PSCollectionView.m +++ b/PSCollectionView.m @@ -386,7 +386,6 @@ - (CGFloat)updateFooterViewWithTotalHeight:(CGFloat)totalHeight if (self.footerView) { self.footerView.width = self.width; self.footerView.top = totalHeight; - [self addSubview:self.footerView]; CGSize footerSize = [self.footerView sizeThatFits:CGSizeMake(self.width, CGFLOAT_MAX)]; self.footerView.height = footerSize.height; @@ -467,6 +466,8 @@ - (void)relayoutViews { [self addSubview:self.emptyView]; } else if (self.headerView) { totalHeight = top; + } else { + totalHeight = top; } } From 2e790ca5308ec6d8e1c495be0a778654e95692ac Mon Sep 17 00:00:00 2001 From: Adam Becevello Date: Fri, 30 Nov 2012 15:30:12 -0500 Subject: [PATCH 28/55] Replace gesture recognizers on each cell with a recognizer for the entire view --- PSCollectionView.m | 46 ++++++++++++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/PSCollectionView.m b/PSCollectionView.m index febe70c..8caa391 100644 --- a/PSCollectionView.m +++ b/PSCollectionView.m @@ -242,6 +242,8 @@ - (id)initWithFrame:(CGRect)frame { self.loadedIndices = [NSMutableIndexSet indexSet]; self.animateFirstCellAppearance = YES; self.headerViewHeight = 0.0f; + + [self addGestureRecognizer:[[PSCollectionViewTapGestureRecognizer alloc] initWithTarget:self action:@selector(didSelectView:)]]; } return self; } @@ -548,14 +550,6 @@ - (void)removeAndAddCellsIfNecessary { } completion:nil]; } } - - // Setup gesture recognizer - if ([newView.gestureRecognizers count] == 0) { - PSCollectionViewTapGestureRecognizer *gr = [[PSCollectionViewTapGestureRecognizer alloc] initWithTarget:self action:@selector(didSelectView:)]; - gr.delegate = self; - [newView addGestureRecognizer:gr]; - newView.userInteractionEnabled = YES; - } [self.visibleViews setObject:newView forKey:key]; } @@ -608,19 +602,35 @@ - (void)enqueueReusableView:(PSCollectionViewCell *)view { #pragma mark - Gesture Recognizer - (void)didSelectView:(UITapGestureRecognizer *)gestureRecognizer { - NSValue *rectValue = [NSValue valueWithCGRect:gestureRecognizer.view.frame]; - NSArray *matchingKeys = [self.indexToRectMap allKeysForObject:rectValue]; - PSCollectionViewKey *key = [matchingKeys lastObject]; - if ([gestureRecognizer.view isMemberOfClass:[[self.visibleViews objectForKey:key] class]]) { - if (self.collectionViewDelegate && [self.collectionViewDelegate respondsToSelector:@selector(collectionView:didSelectView:atIndex:)]) { - NSInteger matchingIndex = PSCollectionIndexForKey([matchingKeys lastObject]); - [self.collectionViewDelegate collectionView:self didSelectView:(PSCollectionViewCell *)gestureRecognizer.view atIndex:matchingIndex]; - } - } + CGPoint tapPoint = [gestureRecognizer locationInView:self]; + + //determine which grid item (if any) this tap was on + PSCollectionViewCell *viewCell = nil; + NSArray *visibleViewArray = [self.visibleViews allValues]; + for (PSCollectionViewCell *view in visibleViewArray) { + if (CGRectContainsPoint(view.frame, tapPoint)) { + viewCell = view; + break; + } + } + + if (viewCell) { + NSValue *rectValue = [NSValue valueWithCGRect:viewCell.frame]; + NSArray *matchingKeys = [self.indexToRectMap allKeysForObject:rectValue]; + PSCollectionViewKey *key = [matchingKeys lastObject]; + if ([viewCell isMemberOfClass:[[self.visibleViews objectForKey:key] class]]) { + if (self.collectionViewDelegate && [self.collectionViewDelegate respondsToSelector:@selector(collectionView:didSelectView:atIndex:)]) { + NSInteger matchingIndex = PSCollectionIndexForKey(key); + [self.collectionViewDelegate collectionView:self didSelectView:viewCell atIndex:matchingIndex]; + } + } + } } - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch { - if (![gestureRecognizer isMemberOfClass:[PSCollectionViewTapGestureRecognizer class]]) return YES; + if (![gestureRecognizer isMemberOfClass:[PSCollectionViewTapGestureRecognizer class]]) { + return YES; + } NSValue *rectValue = [NSValue valueWithCGRect:gestureRecognizer.view.frame]; NSArray *matchingKeys = [self.indexToRectMap allKeysForObject:rectValue]; From c842eae84e7627efda0c5fb75208901c200da94a Mon Sep 17 00:00:00 2001 From: Carson Brown Date: Mon, 3 Dec 2012 14:51:30 -0500 Subject: [PATCH 29/55] Revert "Merge pull request #10 from carsonb/bugfix/remove-multiple-touches" This reverts commit e83b47d7169f6e5d70fc63355baccc715af36c97, reversing changes made to 7c00ef1ca3333a910bf95e95f1b691dbc3b6d938. --- PSCollectionView.m | 46 ++++++++++++++++++---------------------------- 1 file changed, 18 insertions(+), 28 deletions(-) diff --git a/PSCollectionView.m b/PSCollectionView.m index 8caa391..febe70c 100644 --- a/PSCollectionView.m +++ b/PSCollectionView.m @@ -242,8 +242,6 @@ - (id)initWithFrame:(CGRect)frame { self.loadedIndices = [NSMutableIndexSet indexSet]; self.animateFirstCellAppearance = YES; self.headerViewHeight = 0.0f; - - [self addGestureRecognizer:[[PSCollectionViewTapGestureRecognizer alloc] initWithTarget:self action:@selector(didSelectView:)]]; } return self; } @@ -550,6 +548,14 @@ - (void)removeAndAddCellsIfNecessary { } completion:nil]; } } + + // Setup gesture recognizer + if ([newView.gestureRecognizers count] == 0) { + PSCollectionViewTapGestureRecognizer *gr = [[PSCollectionViewTapGestureRecognizer alloc] initWithTarget:self action:@selector(didSelectView:)]; + gr.delegate = self; + [newView addGestureRecognizer:gr]; + newView.userInteractionEnabled = YES; + } [self.visibleViews setObject:newView forKey:key]; } @@ -602,35 +608,19 @@ - (void)enqueueReusableView:(PSCollectionViewCell *)view { #pragma mark - Gesture Recognizer - (void)didSelectView:(UITapGestureRecognizer *)gestureRecognizer { - CGPoint tapPoint = [gestureRecognizer locationInView:self]; - - //determine which grid item (if any) this tap was on - PSCollectionViewCell *viewCell = nil; - NSArray *visibleViewArray = [self.visibleViews allValues]; - for (PSCollectionViewCell *view in visibleViewArray) { - if (CGRectContainsPoint(view.frame, tapPoint)) { - viewCell = view; - break; - } - } - - if (viewCell) { - NSValue *rectValue = [NSValue valueWithCGRect:viewCell.frame]; - NSArray *matchingKeys = [self.indexToRectMap allKeysForObject:rectValue]; - PSCollectionViewKey *key = [matchingKeys lastObject]; - if ([viewCell isMemberOfClass:[[self.visibleViews objectForKey:key] class]]) { - if (self.collectionViewDelegate && [self.collectionViewDelegate respondsToSelector:@selector(collectionView:didSelectView:atIndex:)]) { - NSInteger matchingIndex = PSCollectionIndexForKey(key); - [self.collectionViewDelegate collectionView:self didSelectView:viewCell atIndex:matchingIndex]; - } - } - } + NSValue *rectValue = [NSValue valueWithCGRect:gestureRecognizer.view.frame]; + NSArray *matchingKeys = [self.indexToRectMap allKeysForObject:rectValue]; + PSCollectionViewKey *key = [matchingKeys lastObject]; + if ([gestureRecognizer.view isMemberOfClass:[[self.visibleViews objectForKey:key] class]]) { + if (self.collectionViewDelegate && [self.collectionViewDelegate respondsToSelector:@selector(collectionView:didSelectView:atIndex:)]) { + NSInteger matchingIndex = PSCollectionIndexForKey([matchingKeys lastObject]); + [self.collectionViewDelegate collectionView:self didSelectView:(PSCollectionViewCell *)gestureRecognizer.view atIndex:matchingIndex]; + } + } } - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch { - if (![gestureRecognizer isMemberOfClass:[PSCollectionViewTapGestureRecognizer class]]) { - return YES; - } + if (![gestureRecognizer isMemberOfClass:[PSCollectionViewTapGestureRecognizer class]]) return YES; NSValue *rectValue = [NSValue valueWithCGRect:gestureRecognizer.view.frame]; NSArray *matchingKeys = [self.indexToRectMap allKeysForObject:rectValue]; From cb919f971748cb77937e04a16ad937b3c3841456 Mon Sep 17 00:00:00 2001 From: Adam Becevello Date: Tue, 11 Dec 2012 15:34:09 -0500 Subject: [PATCH 30/55] Improved handling of gesture recognizer to handle selection on headers This reverts commit c842eae84e7627efda0c5fb75208901c200da94a. --- PSCollectionView.m | 48 +++++++++++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/PSCollectionView.m b/PSCollectionView.m index febe70c..6f35193 100644 --- a/PSCollectionView.m +++ b/PSCollectionView.m @@ -242,6 +242,10 @@ - (id)initWithFrame:(CGRect)frame { self.loadedIndices = [NSMutableIndexSet indexSet]; self.animateFirstCellAppearance = YES; self.headerViewHeight = 0.0f; + + PSCollectionViewTapGestureRecognizer *recognizer = [[PSCollectionViewTapGestureRecognizer alloc] initWithTarget:self action:@selector(didSelectView:)]; + [recognizer setCancelsTouchesInView:NO]; + [self addGestureRecognizer:recognizer]; } return self; } @@ -548,14 +552,6 @@ - (void)removeAndAddCellsIfNecessary { } completion:nil]; } } - - // Setup gesture recognizer - if ([newView.gestureRecognizers count] == 0) { - PSCollectionViewTapGestureRecognizer *gr = [[PSCollectionViewTapGestureRecognizer alloc] initWithTarget:self action:@selector(didSelectView:)]; - gr.delegate = self; - [newView addGestureRecognizer:gr]; - newView.userInteractionEnabled = YES; - } [self.visibleViews setObject:newView forKey:key]; } @@ -608,19 +604,35 @@ - (void)enqueueReusableView:(PSCollectionViewCell *)view { #pragma mark - Gesture Recognizer - (void)didSelectView:(UITapGestureRecognizer *)gestureRecognizer { - NSValue *rectValue = [NSValue valueWithCGRect:gestureRecognizer.view.frame]; - NSArray *matchingKeys = [self.indexToRectMap allKeysForObject:rectValue]; - PSCollectionViewKey *key = [matchingKeys lastObject]; - if ([gestureRecognizer.view isMemberOfClass:[[self.visibleViews objectForKey:key] class]]) { - if (self.collectionViewDelegate && [self.collectionViewDelegate respondsToSelector:@selector(collectionView:didSelectView:atIndex:)]) { - NSInteger matchingIndex = PSCollectionIndexForKey([matchingKeys lastObject]); - [self.collectionViewDelegate collectionView:self didSelectView:(PSCollectionViewCell *)gestureRecognizer.view atIndex:matchingIndex]; - } - } + CGPoint tapPoint = [gestureRecognizer locationInView:self]; + + //determine which grid item (if any) this tap was on + PSCollectionViewCell *viewCell = nil; + NSArray *visibleViewArray = [self.visibleViews allValues]; + for (PSCollectionViewCell *view in visibleViewArray) { + if (CGRectContainsPoint(view.frame, tapPoint)) { + viewCell = view; + break; + } + } + + if (viewCell) { + NSValue *rectValue = [NSValue valueWithCGRect:viewCell.frame]; + NSArray *matchingKeys = [self.indexToRectMap allKeysForObject:rectValue]; + PSCollectionViewKey *key = [matchingKeys lastObject]; + if ([viewCell isMemberOfClass:[[self.visibleViews objectForKey:key] class]]) { + if (self.collectionViewDelegate && [self.collectionViewDelegate respondsToSelector:@selector(collectionView:didSelectView:atIndex:)]) { + NSInteger matchingIndex = PSCollectionIndexForKey(key); + [self.collectionViewDelegate collectionView:self didSelectView:viewCell atIndex:matchingIndex]; + } + } + } } - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch { - if (![gestureRecognizer isMemberOfClass:[PSCollectionViewTapGestureRecognizer class]]) return YES; + if ([gestureRecognizer isMemberOfClass:[PSCollectionViewTapGestureRecognizer class]] == NO) { + return YES; + } NSValue *rectValue = [NSValue valueWithCGRect:gestureRecognizer.view.frame]; NSArray *matchingKeys = [self.indexToRectMap allKeysForObject:rectValue]; From f125d1f782cf58afcbead66427470605244556e9 Mon Sep 17 00:00:00 2001 From: Adam Becevello Date: Fri, 14 Dec 2012 16:54:46 -0500 Subject: [PATCH 31/55] Start of rewriting grid view layout code to support additional things --- PSCollectionView.h | 18 +- PSCollectionView.m | 384 ++++++++++++++++++++--------- PSCollectionViewLayoutAttributes.h | 21 ++ PSCollectionViewLayoutAttributes.m | 25 ++ 4 files changed, 331 insertions(+), 117 deletions(-) create mode 100644 PSCollectionViewLayoutAttributes.h create mode 100644 PSCollectionViewLayoutAttributes.m diff --git a/PSCollectionView.h b/PSCollectionView.h index fed2f71..f10ccd1 100644 --- a/PSCollectionView.h +++ b/PSCollectionView.h @@ -41,12 +41,14 @@ @property (nonatomic, assign, readonly) NSInteger numCols; @property (nonatomic, assign) NSInteger numColsLandscape; @property (nonatomic, assign) NSInteger numColsPortrait; -@property (nonatomic, assign) BOOL animateFirstCellAppearance; -@property (nonatomic, unsafe_unretained) id collectionViewDelegate; -@property (nonatomic, unsafe_unretained) id collectionViewDataSource; +@property (nonatomic, assign) BOOL animateLayoutChanges; +@property (nonatomic, weak) id collectionViewDelegate; +@property (nonatomic, weak) id collectionViewDataSource; #pragma mark - Public Methods +- (void)invalidateLayout; + /** Reloads the collection view This is similar to UITableView reloadData) @@ -59,10 +61,12 @@ */ - (UIView *)dequeueReusableView; -/** - Tells the collection view one more PSCollectionViewCell is added to the end - */ -- (void)appendView; +- (void)insertItem; +- (void)insertItemAtIndex:(NSUInteger)index; +- (void)removeItemAtIndex:(NSUInteger)index; +//TODO: support moving? + +- (void)performBatchUpdates:(void (^)(void))updates completion:(void (^)(void))completion; @end diff --git a/PSCollectionView.m b/PSCollectionView.m index 6f35193..7f205f2 100644 --- a/PSCollectionView.m +++ b/PSCollectionView.m @@ -23,6 +23,7 @@ #import "PSCollectionView.h" #import "PSCollectionViewCell.h" +#import "PSCollectionViewLayoutAttributes.h" #define kDefaultMargin 8.0 #define kAnimationDuration 0.3f @@ -169,57 +170,19 @@ @interface PSCollectionView () @property (nonatomic, assign, readwrite) CGFloat headerViewHeight; -/** - Forces a relayout of the collection grid - */ -- (void)relayoutViews; - -/** - Stores a view for later reuse - TODO: add an identifier like UITableView - */ -- (void)enqueueReusableView:(PSCollectionViewCell *)view; - -/** - Magic! - */ -- (void)removeAndAddCellsIfNecessary; - @end @implementation PSCollectionView { BOOL _resetLoadedIndices; + BOOL _batchUpdateInProgress; + BOOL _layoutInvalidated; + + NSMutableArray *_colXOffsets; + NSMutableArray *_colHeights; + + NSMutableArray *_items; //position is by index, value is PSCollectionViewLayoutAttribute objects } -// Public Views -@synthesize -headerView = _headerView, -footerView = _footerView, -emptyView = _emptyView, -loadingView = _loadingView; - -// Public -@synthesize -margin = _margin, -colWidth = _colWidth, -numCols = _numCols, -numColsLandscape = _numColsLandscape, -numColsPortrait = _numColsPortrait, -animateFirstCellAppearance = _animateFirstCellAppearance, -collectionViewDelegate = _collectionViewDelegate, -collectionViewDataSource = _collectionViewDataSource; - -// Private -@synthesize -orientation = _orientation, -reuseableViews = _reuseableViews, -visibleViews = _visibleViews, -viewKeysToRemove = _viewKeysToRemove, -indexToRectMap = _indexToRectMap, -colOffsets = _colOffsets, -loadedIndices = _loadedIndices, -headerViewHeight = _headerViewHeight; - #pragma mark - Init/Memory - (id)initWithFrame:(CGRect)frame { @@ -240,12 +203,15 @@ - (id)initWithFrame:(CGRect)frame { self.viewKeysToRemove = [NSMutableArray array]; self.indexToRectMap = [NSMutableDictionary dictionary]; self.loadedIndices = [NSMutableIndexSet indexSet]; - self.animateFirstCellAppearance = YES; + self.animateLayoutChanges = YES; self.headerViewHeight = 0.0f; PSCollectionViewTapGestureRecognizer *recognizer = [[PSCollectionViewTapGestureRecognizer alloc] initWithTarget:self action:@selector(didSelectView:)]; [recognizer setCancelsTouchesInView:NO]; [self addGestureRecognizer:recognizer]; + + _items = [NSMutableArray array]; + [self invalidateLayout]; } return self; } @@ -259,78 +225,263 @@ - (void)dealloc { #pragma mark - Setters +- (void)setNumColsLandscape:(NSInteger)numColsLandscape +{ + _numColsLandscape = numColsLandscape; + [self invalidateLayout]; +} + +- (void)setNumColsPortrait:(NSInteger)numColsPortrait +{ + _numColsPortrait = numColsPortrait; + [self invalidateLayout]; +} + - (void)setLoadingView:(UIView *)loadingView { - if (_loadingView) { - [_loadingView removeFromSuperview]; - } - _loadingView = loadingView; - - [self addSubview:_loadingView]; + [_loadingView removeFromSuperview]; + _loadingView = nil; - [self relayoutViews]; + if (loadingView) { + _loadingView = loadingView; + [self addSubview:_loadingView]; + } + + [self invalidateLayout]; } - (void)setEmptyView:(UIView *)emptyView { - if (_emptyView) { - [_emptyView removeFromSuperview]; - } - _emptyView = emptyView; + [_emptyView removeFromSuperview]; + _emptyView = nil; - [self addSubview:_emptyView]; + if (emptyView) { + _emptyView = emptyView; + [self addSubview:_emptyView]; + } [self relayoutViews]; } - (void)setHeaderView:(UIView *)headerView { - if (_headerView) { - [_headerView removeFromSuperview]; - } - _headerView = headerView; + [_headerView removeFromSuperview]; + _headerView = nil; - [self addSubview:_headerView]; + if (headerView) { + _headerView = headerView; + [self addSubview:_headerView]; + } - [self relayoutViews]; + [self invalidateLayout]; } - (void)setFooterView:(UIView *)footerView { - if (_footerView) { - [_footerView removeFromSuperview]; - } - _footerView = footerView; + [_footerView removeFromSuperview]; + _footerView = nil; - [self addSubview:_footerView]; + if (footerView) { + _footerView = footerView; + [self addSubview:_footerView]; + } - [self relayoutViews]; + [self invalidateLayout]; } #pragma mark - DataSource - (void)reloadData { _resetLoadedIndices = YES; - [self relayoutViews]; + [self invalidateLayout]; } #pragma mark - View -- (void)layoutSubviews { +- (void)invalidateLayout +{ + [_items enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(PSCollectionViewLayoutAttributes *attributes, NSUInteger idx, BOOL *stop) { + attributes.valid = NO; + }]; + + self.numCols = UIInterfaceOrientationIsPortrait(self.orientation) ? self.numColsPortrait : self.numColsLandscape; + + //reset the column offsets + _colXOffsets = nil; + + _colHeights = [NSMutableArray arrayWithCapacity:self.numCols]; + for (int i = 0; i < self.numCols; i++) { + [_colHeights addObject:@(self.headerViewHeight + self.margin)]; + } + + self.colWidth = floorf((self.width - self.margin * (self.numCols + 1)) / self.numCols); + + _layoutInvalidated = YES; +} + +- (void)layoutSubviews +{ [super layoutSubviews]; - + UIInterfaceOrientation orientation = [UIApplication sharedApplication].statusBarOrientation; if (self.orientation != orientation) { self.orientation = orientation; - [self relayoutViews]; - } else { - //determine if the header has changed height - CGSize headerSize = [self.headerView sizeThatFits:CGSizeMake(self.width, CGFLOAT_MAX)]; - if (self.headerViewHeight != headerSize.height) { - self.headerView.height = headerSize.height; - self.headerViewHeight = headerSize.height; + + [self invalidateLayout]; +// } else { +// //determine if the header has changed height +// CGSize headerSize = [self.headerView sizeThatFits:CGSizeMake(self.width, CGFLOAT_MAX)]; +// if (self.headerViewHeight != headerSize.height) { +// self.headerView.height = headerSize.height; +// self.headerViewHeight = headerSize.height; +// +// //need to adjust all the cells and column heights to reflect the new header height +// [self relayoutViews]; +// } +// +// [self removeAndAddCellsIfNecessary]; + } + + //TODO: determine if performLayout needs to be done on layoutSubviews + + //determine if the header has changed height + CGSize headerSize = [self.headerView sizeThatFits:CGSizeMake(self.width, CGFLOAT_MAX)]; + if (self.headerViewHeight != headerSize.height) { + self.headerView.height = headerSize.height; + self.headerViewHeight = headerSize.height; + + //need to adjust all the cells and column heights to reflect the new header height + [self invalidateLayout]; + } + + //TODO: put in animation block? + if (_layoutInvalidated) { + [self performLayout]; + } +} + +- (void)performLayout +{ + if (_colXOffsets == nil) { + _colXOffsets = [NSMutableArray arrayWithCapacity:self.numCols]; + CGFloat left = self.margin; + for (int i = 0; i < self.numCols; i++) { + [_colXOffsets addObject:@(left)]; + left += self.colWidth + self.margin; + } + } + + //TODO: handle empty data view + + //TODO: layout header + + //calculate the positions of all cells, but skip the cells that had no change + //a cell has a change if its layout is marked as invalid + [_items enumerateObjectsUsingBlock:^(PSCollectionViewLayoutAttributes *itemAttributes, NSUInteger idx, BOOL *stop) { + if (itemAttributes.valid == NO) { + //ensure we have the height for this item + CGFloat height = itemAttributes.frame.size.height; + if (height == 0.0f) { + height = [self.collectionViewDataSource heightForViewAtIndex:idx]; + } + + //find the shortest column + NSUInteger shortestColumn = [self shortestColumn]; + CGFloat colXOffset = [_colXOffsets[shortestColumn] floatValue]; + CGFloat colHeight = [_colHeights[shortestColumn] floatValue]; + CGRect frame = CGRectMake(colXOffset, colHeight, self.colWidth, height); + itemAttributes.frame = frame; + itemAttributes.currentColumn = shortestColumn; + itemAttributes.alpha = 1.0f; + itemAttributes.valid = YES; + itemAttributes.cell.frame = frame; - //need to adjust all the cells and column heights to reflect the new header height - [self relayoutViews]; + //update the column heights + _colHeights[shortestColumn] = @(colHeight + height + self.margin); } - - [self removeAndAddCellsIfNecessary]; + }]; + + //TODO: update footer position + + //update the content size + NSUInteger longestColumn = [self longestColumn]; + self.contentSize = CGSizeMake(self.width, [_colHeights[longestColumn] floatValue]); + + _layoutInvalidated = NO; +} + +- (void)insertItem +{ + [self insertItemAtIndex:[_items count]]; +} + +- (void)insertItemAtIndex:(NSUInteger)index +{ + PSCollectionViewCell *cell = [self.collectionViewDataSource collectionView:self viewAtIndex:index]; + PSCollectionViewLayoutAttributes *attributes = [[PSCollectionViewLayoutAttributes alloc] init]; + attributes.cell = cell; + + //if we are animating layout changes, fade in the cell + if (self.animateLayoutChanges) { + attributes.alpha = 0.0f; + } + + [_items insertObject:attributes atIndex:index]; + [self addSubview:cell]; + + [self invalidateLayoutOfItemsAfterIndex:index]; + + //erform layout if not in a batch update + if (_batchUpdateInProgress == NO) { + [self performLayout]; + } +} + +- (void)removeItemAtIndex:(NSUInteger)index +{ + PSCollectionViewLayoutAttributes *attributes = _items[index]; + [self enqueueReusableView:attributes.cell]; + + [_items removeObjectAtIndex:index]; + [self invalidateLayoutOfItemsAfterIndex:index]; + + //update the column heights + [self updateHeightOfColumnsAtIndex:index]; + + //perform layout if not in a batch update + if (_batchUpdateInProgress == NO) { + [self performLayout]; + } +} + +- (void)performBatchUpdates:(void (^)(void))updates completion:(void (^)(void))completion +{ + _batchUpdateInProgress = YES; + [UIView animateWithDuration:kAnimationDuration animations:^{ + if (updates) { + updates(); + } + //perform layout to apply the changes + [self performLayout]; + } completion:^(BOOL finished) { + _batchUpdateInProgress = NO; + if (completion) { + completion(); + } + }]; +} + +- (void)updateHeightOfColumnsAtIndex:(NSUInteger)index +{ + //get the max Y values from the previous elements in each column (only need to get numCols number of elements) + //TODO: make sure this is iterating the correct number of times + for (int i = index; i > index - self.numCols && i >=0; i--) { + PSCollectionViewLayoutAttributes *attributes = _items[i]; + _colHeights[attributes.currentColumn] = @(CGRectGetMaxY(attributes.frame) + self.margin); + } +} + +- (void)invalidateLayoutOfItemsAfterIndex:(NSUInteger)index +{ + for (int i=index; i < [_items count]; i++) { + PSCollectionViewLayoutAttributes *attributes = _items[i]; + attributes.valid = NO; } } @@ -363,24 +514,15 @@ - (void)insertViewRectForIndex:(int)index forKey:(id )key inColumn:(N CGFloat top = [[_colOffsets objectAtIndex:col] floatValue]; CGFloat colHeight = [self.collectionViewDataSource heightForViewAtIndex:index]; if (colHeight == 0) { - colHeight = self.colWidth; + colHeight = self.margin; } - if (top != top) { - // NaN - } - - CGRect viewRect = CGRectMake(left, top, self.colWidth, colHeight); - // Add to index rect map + CGRect viewRect = CGRectMake(left, top, self.colWidth, colHeight); [self.indexToRectMap setObject:[NSValue valueWithCGRect:viewRect] forKey:key]; // Update the last height offset for this column CGFloat test = top + colHeight + self.margin; - - if (test != test) { - // NaN - } [_colOffsets replaceObjectAtIndex:col withObject:[NSNumber numberWithFloat:test]]; } @@ -488,8 +630,9 @@ - (void)removeAndAddCellsIfNecessary { static NSInteger bottomIndex = 0; NSInteger numViews = [self.collectionViewDataSource numberOfViewsInCollectionView:self]; - - if (numViews == 0) return; + if (numViews == 0) { + return; + } // Find out what rows are visible CGRect visibleRect = CGRectMake(self.contentOffset.x, self.contentOffset.y, self.width, self.height); @@ -498,7 +641,7 @@ - (void)removeAndAddCellsIfNecessary { [self.visibleViews enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { PSCollectionViewCell *view = (PSCollectionViewCell *)obj; CGRect viewRect = view.frame; - if (!CGRectIntersectsRect(visibleRect, viewRect)) { + if (CGRectIntersectsRect(visibleRect, viewRect) == NO) { [self enqueueReusableView:view]; [self.viewKeysToRemove addObject:key]; } @@ -528,7 +671,6 @@ - (void)removeAndAddCellsIfNecessary { topIndex = MAX(0, topIndex - (bufferViewFactor * self.numCols)); bottomIndex = MIN(numViews, bottomIndex + (bufferViewFactor * self.numCols)); } - // NSLog(@"topIndex: %d, bottomIndex: %d", topIndex, bottomIndex); // Add views for (NSInteger i = topIndex; i < bottomIndex; i++) { @@ -545,7 +687,7 @@ - (void)removeAndAddCellsIfNecessary { } else { //animate it in, add it to the set [self.loadedIndices addIndex:i]; [self addSubview:newView]; - if (self.animateFirstCellAppearance) { + if (self.animateLayoutChanges) { newView.alpha = 0.0f; [UIView animateWithDuration:kAnimationDuration delay:0.0f options:UIViewAnimationOptionAllowUserInteraction animations:^{ newView.alpha = 1.0f; @@ -579,6 +721,36 @@ - (void)appendView } } +#pragma mark - Helpers + +- (NSUInteger)shortestColumn +{ + NSInteger col = 0; + CGFloat minHeight = [_colHeights[col] floatValue]; + for (int i = 1; i < [_colHeights count]; i++) { + CGFloat colHeight = [_colHeights[i] floatValue]; + if (colHeight < minHeight) { + col = i; + minHeight = colHeight; + } + } + return col; +} + +- (NSUInteger)longestColumn +{ + NSInteger col = 0; + CGFloat maxHeight = [_colHeights[col] floatValue]; + for (int i = 1; i < [_colHeights count]; i++) { + CGFloat colHeight = [_colHeights[i] floatValue]; + if (colHeight > maxHeight) { + col = i; + maxHeight = colHeight; + } + } + return col; +} + #pragma mark - Reusing Views - (PSCollectionViewCell *)dequeueReusableView { @@ -587,14 +759,11 @@ - (PSCollectionViewCell *)dequeueReusableView { // Found a reusable view, remove it from the set [self.reuseableViews removeObject:view]; } - return view; } - (void)enqueueReusableView:(PSCollectionViewCell *)view { - if ([view respondsToSelector:@selector(prepareForReuse)]) { - [view performSelector:@selector(prepareForReuse)]; - } + [view prepareForReuse]; view.frame = CGRectZero; view.alpha = 1.0f; [self.reuseableViews addObject:view]; @@ -637,12 +806,7 @@ - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceive NSValue *rectValue = [NSValue valueWithCGRect:gestureRecognizer.view.frame]; NSArray *matchingKeys = [self.indexToRectMap allKeysForObject:rectValue]; NSString *key = [matchingKeys lastObject]; - - if ([touch.view isMemberOfClass:[[self.visibleViews objectForKey:key] class]]) { - return YES; - } else { - return NO; - } + return [touch.view isMemberOfClass:[[self.visibleViews objectForKey:key] class]]; } @end diff --git a/PSCollectionViewLayoutAttributes.h b/PSCollectionViewLayoutAttributes.h new file mode 100644 index 0000000..d95a61d --- /dev/null +++ b/PSCollectionViewLayoutAttributes.h @@ -0,0 +1,21 @@ +// +// PSCollectionViewLayoutAttributes.h +// ShopByShopify +// +// Created by Adam Becevello on 2012-12-14. +// Copyright (c) 2012 Shopify Inc. All rights reserved. +// + +#import +#import "PSCollectionViewCell.h" + +@interface PSCollectionViewLayoutAttributes : NSObject + +@property (nonatomic, assign) CGRect frame; +@property (nonatomic, assign) NSUInteger currentColumn; +@property (nonatomic, assign) CGFloat alpha; +@property (nonatomic, assign) BOOL valid; + +@property (nonatomic, strong) PSCollectionViewCell *cell; + +@end diff --git a/PSCollectionViewLayoutAttributes.m b/PSCollectionViewLayoutAttributes.m new file mode 100644 index 0000000..f70d27b --- /dev/null +++ b/PSCollectionViewLayoutAttributes.m @@ -0,0 +1,25 @@ +// +// PSCollectionViewLayoutAttributes.m +// ShopByShopify +// +// Created by Adam Becevello on 2012-12-14. +// Copyright (c) 2012 Shopify Inc. All rights reserved. +// + +#import "PSCollectionViewLayoutAttributes.h" + +@implementation PSCollectionViewLayoutAttributes + +- (id)init +{ + self = [super init]; + if (self) { + self.frame = CGRectZero; + self.currentColumn = 0; + self.alpha = 1.0f; + self.valid = NO; + } + return self; +} + +@end From 0705bbe3c99ea5a621728cf77855151a92ca5e54 Mon Sep 17 00:00:00 2001 From: Adam Becevello Date: Sat, 15 Dec 2012 14:50:26 -0500 Subject: [PATCH 32/55] Minor changes --- PSCollectionView.m | 38 ++++++++++++-------------------------- 1 file changed, 12 insertions(+), 26 deletions(-) diff --git a/PSCollectionView.m b/PSCollectionView.m index 7f205f2..a8d2b49 100644 --- a/PSCollectionView.m +++ b/PSCollectionView.m @@ -287,7 +287,8 @@ - (void)setFooterView:(UIView *)footerView { #pragma mark - DataSource -- (void)reloadData { +- (void)reloadData +{ _resetLoadedIndices = YES; [self invalidateLayout]; } @@ -485,6 +486,8 @@ - (void)invalidateLayoutOfItemsAfterIndex:(NSUInteger)index } } +#pragma mark - Old code after this in the section + - (void)buildColumnOffsetsFromTop:(CGFloat) top { self.colOffsets = [NSMutableArray arrayWithCapacity:self.numCols]; @@ -700,27 +703,6 @@ - (void)removeAndAddCellsIfNecessary { } } -- (void)appendView -{ - NSInteger numViews = [self.collectionViewDataSource numberOfViewsInCollectionView:self]; - if ([self.indexToRectMap count] == 0 || numViews == 1) { - //just build via a reload - [self reloadData]; - } else { - PSCollectionViewKey *key = PSCollectionKeyForIndex(numViews-1); - - // Find the shortest column - NSInteger col = [self findShortestColumn]; - [self insertViewRectForIndex:numViews-1 forKey:key inColumn:col]; - CGFloat totalHeight = [self totalHeightFromColOffsetsWithTotalHeight:0.0f]; - - totalHeight = [self updateFooterViewWithTotalHeight:totalHeight]; - - self.contentSize = CGSizeMake(self.width, totalHeight); - [self removeAndAddCellsIfNecessary]; - } -} - #pragma mark - Helpers - (NSUInteger)shortestColumn @@ -753,7 +735,8 @@ - (NSUInteger)longestColumn #pragma mark - Reusing Views -- (PSCollectionViewCell *)dequeueReusableView { +- (PSCollectionViewCell *)dequeueReusableView +{ PSCollectionViewCell *view = [self.reuseableViews anyObject]; if (view) { // Found a reusable view, remove it from the set @@ -762,7 +745,8 @@ - (PSCollectionViewCell *)dequeueReusableView { return view; } -- (void)enqueueReusableView:(PSCollectionViewCell *)view { +- (void)enqueueReusableView:(PSCollectionViewCell *)view +{ [view prepareForReuse]; view.frame = CGRectZero; view.alpha = 1.0f; @@ -772,7 +756,8 @@ - (void)enqueueReusableView:(PSCollectionViewCell *)view { #pragma mark - Gesture Recognizer -- (void)didSelectView:(UITapGestureRecognizer *)gestureRecognizer { +- (void)didSelectView:(UITapGestureRecognizer *)gestureRecognizer +{ CGPoint tapPoint = [gestureRecognizer locationInView:self]; //determine which grid item (if any) this tap was on @@ -798,7 +783,8 @@ - (void)didSelectView:(UITapGestureRecognizer *)gestureRecognizer { } } -- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch { +- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch +{ if ([gestureRecognizer isMemberOfClass:[PSCollectionViewTapGestureRecognizer class]] == NO) { return YES; } From bccbf315f0853c8cc6b3ce4c1390eaacc801c116 Mon Sep 17 00:00:00 2001 From: Adam Becevello Date: Mon, 17 Dec 2012 16:58:52 -0500 Subject: [PATCH 33/55] Reenable cell reuse, handle insertions and removals --- PSCollectionView.h | 3 +- PSCollectionView.m | 553 ++++++++--------------------- PSCollectionViewLayoutAttributes.h | 3 +- PSCollectionViewLayoutAttributes.m | 1 + 4 files changed, 160 insertions(+), 400 deletions(-) diff --git a/PSCollectionView.h b/PSCollectionView.h index f10ccd1..d52d741 100644 --- a/PSCollectionView.h +++ b/PSCollectionView.h @@ -61,10 +61,9 @@ */ - (UIView *)dequeueReusableView; -- (void)insertItem; +- (void)insertItemAtEnd; - (void)insertItemAtIndex:(NSUInteger)index; - (void)removeItemAtIndex:(NSUInteger)index; -//TODO: support moving? - (void)performBatchUpdates:(void (^)(void))updates completion:(void (^)(void))completion; diff --git a/PSCollectionView.m b/PSCollectionView.m index a8d2b49..d0e8e81 100644 --- a/PSCollectionView.m +++ b/PSCollectionView.m @@ -28,57 +28,7 @@ #define kDefaultMargin 8.0 #define kAnimationDuration 0.3f -@interface PSCollectionViewKey : NSObject - -- (instancetype)initWithInteger:(NSInteger)integer; -+ (PSCollectionViewKey*)keyWithInteger:(NSInteger)integer; - -@property (nonatomic, assign) NSInteger key; - -@end - -@implementation PSCollectionViewKey - -- (id)copyWithZone:(NSZone *)zone -{ - PSCollectionViewKey *newKey = [[self class] allocWithZone:zone]; - newKey.key = self.key; - return newKey; -} - -- (instancetype)initWithInteger:(NSInteger)integer -{ - self = [super init]; - if (self) { - _key = integer; - } - return self; -} - -+ (PSCollectionViewKey*)keyWithInteger:(NSInteger)integer -{ - return [[PSCollectionViewKey alloc] initWithInteger:integer]; -} - -- (NSUInteger)hash -{ - return _key; -} - -- (BOOL)isEqual:(id)object -{ - return _key == ((PSCollectionViewKey*)object).key; -} - -@end - -static inline PSCollectionViewKey * PSCollectionKeyForIndex(NSInteger index) { - return [PSCollectionViewKey keyWithInteger:index]; -} - -static inline NSInteger PSCollectionIndexForKey(PSCollectionViewKey *key) { - return key.key; -} +#define kPSCollectionViewCellReuseBufferRows 5 #pragma mark - UIView Category @@ -155,37 +105,31 @@ @implementation PSCollectionViewTapGestureRecognizer @end -@interface PSCollectionView () +@interface PSCollectionView () @property (nonatomic, assign, readwrite) CGFloat colWidth; @property (nonatomic, assign, readwrite) NSInteger numCols; -@property (nonatomic, assign) UIInterfaceOrientation orientation; - -@property (nonatomic, strong) NSMutableSet *reuseableViews; -@property (nonatomic, strong) NSMutableDictionary *visibleViews; -@property (nonatomic, strong) NSMutableArray *viewKeysToRemove; -@property (nonatomic, strong) NSMutableDictionary *indexToRectMap; -@property (nonatomic, strong) NSMutableArray *colOffsets; -@property (nonatomic, strong) NSMutableIndexSet *loadedIndices; - -@property (nonatomic, assign, readwrite) CGFloat headerViewHeight; @end @implementation PSCollectionView { - BOOL _resetLoadedIndices; BOOL _batchUpdateInProgress; - BOOL _layoutInvalidated; + + UIInterfaceOrientation _orientation; NSMutableArray *_colXOffsets; NSMutableArray *_colHeights; + NSMutableSet *_reuseableViews; + + NSMutableIndexSet *_visibleItems; NSMutableArray *_items; //position is by index, value is PSCollectionViewLayoutAttribute objects } #pragma mark - Init/Memory -- (id)initWithFrame:(CGRect)frame { +- (id)initWithFrame:(CGRect)frame +{ self = [super initWithFrame:frame]; if (self) { self.alwaysBounceVertical = YES; @@ -196,27 +140,24 @@ - (id)initWithFrame:(CGRect)frame { self.numCols = 0; self.numColsPortrait = 0; self.numColsLandscape = 0; - self.orientation = [UIApplication sharedApplication].statusBarOrientation; - - self.reuseableViews = [NSMutableSet set]; - self.visibleViews = [NSMutableDictionary dictionary]; - self.viewKeysToRemove = [NSMutableArray array]; - self.indexToRectMap = [NSMutableDictionary dictionary]; - self.loadedIndices = [NSMutableIndexSet indexSet]; self.animateLayoutChanges = YES; - self.headerViewHeight = 0.0f; + + _orientation = [UIApplication sharedApplication].statusBarOrientation; + _reuseableViews = [NSMutableSet set]; + _visibleItems = [NSMutableIndexSet indexSet]; + _items = [NSMutableArray array]; PSCollectionViewTapGestureRecognizer *recognizer = [[PSCollectionViewTapGestureRecognizer alloc] initWithTarget:self action:@selector(didSelectView:)]; [recognizer setCancelsTouchesInView:NO]; [self addGestureRecognizer:recognizer]; - _items = [NSMutableArray array]; [self invalidateLayout]; } return self; } -- (void)dealloc { +- (void)dealloc +{ // clear delegates self.delegate = nil; self.collectionViewDataSource = nil; @@ -255,10 +196,11 @@ - (void)setEmptyView:(UIView *)emptyView { if (emptyView) { _emptyView = emptyView; + _emptyView.hidden = YES; [self addSubview:_emptyView]; } - [self relayoutViews]; + [self invalidateLayout]; } - (void)setHeaderView:(UIView *)headerView { @@ -289,7 +231,6 @@ - (void)setFooterView:(UIView *)footerView { - (void)reloadData { - _resetLoadedIndices = YES; [self invalidateLayout]; } @@ -297,23 +238,26 @@ - (void)reloadData - (void)invalidateLayout { + [_visibleItems removeAllIndexes]; [_items enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(PSCollectionViewLayoutAttributes *attributes, NSUInteger idx, BOOL *stop) { attributes.valid = NO; }]; - self.numCols = UIInterfaceOrientationIsPortrait(self.orientation) ? self.numColsPortrait : self.numColsLandscape; + self.numCols = UIInterfaceOrientationIsPortrait(_orientation) ? self.numColsPortrait : self.numColsLandscape; //reset the column offsets _colXOffsets = nil; + [self resetColumnHeights]; + self.colWidth = floorf((self.width - self.margin * (self.numCols + 1)) / self.numCols); +} + +- (void)resetColumnHeights +{ _colHeights = [NSMutableArray arrayWithCapacity:self.numCols]; for (int i = 0; i < self.numCols; i++) { - [_colHeights addObject:@(self.headerViewHeight + self.margin)]; + [_colHeights addObject:@(self.headerView.height + self.margin)]; } - - self.colWidth = floorf((self.width - self.margin * (self.numCols + 1)) / self.numCols); - - _layoutInvalidated = YES; } - (void)layoutSubviews @@ -321,44 +265,37 @@ - (void)layoutSubviews [super layoutSubviews]; UIInterfaceOrientation orientation = [UIApplication sharedApplication].statusBarOrientation; - if (self.orientation != orientation) { - self.orientation = orientation; - + if (_orientation != orientation) { + _orientation = orientation; [self invalidateLayout]; -// } else { -// //determine if the header has changed height -// CGSize headerSize = [self.headerView sizeThatFits:CGSizeMake(self.width, CGFLOAT_MAX)]; -// if (self.headerViewHeight != headerSize.height) { -// self.headerView.height = headerSize.height; -// self.headerViewHeight = headerSize.height; -// -// //need to adjust all the cells and column heights to reflect the new header height -// [self relayoutViews]; -// } -// -// [self removeAndAddCellsIfNecessary]; } - //TODO: determine if performLayout needs to be done on layoutSubviews + [self performLayout]; +} + +- (void)performLayout +{ + __block BOOL recalculateContentSize = NO; - //determine if the header has changed height + //layout header CGSize headerSize = [self.headerView sizeThatFits:CGSizeMake(self.width, CGFLOAT_MAX)]; - if (self.headerViewHeight != headerSize.height) { + if (self.headerView.height != headerSize.height) { self.headerView.height = headerSize.height; - self.headerViewHeight = headerSize.height; + self.headerView.width = self.width; - //need to adjust all the cells and column heights to reflect the new header height + //since the height was changed, the layout needs to be adjusted to handle it [self invalidateLayout]; + recalculateContentSize = YES; } - //TODO: put in animation block? - if (_layoutInvalidated) { - [self performLayout]; + //handle displaying and hiding the empty view + if ([_items count] == 0 && self.emptyView) { + self.emptyView.frame = CGRectMake(self.margin, self.margin, self.width - self.margin * 2, self.height - self.margin * 2); + self.emptyView.hidden = NO; + } else { + self.emptyView.hidden = YES; } -} - -- (void)performLayout -{ + if (_colXOffsets == nil) { _colXOffsets = [NSMutableArray arrayWithCapacity:self.numCols]; CGFloat left = self.margin; @@ -368,10 +305,6 @@ - (void)performLayout } } - //TODO: handle empty data view - - //TODO: layout header - //calculate the positions of all cells, but skip the cells that had no change //a cell has a change if its layout is marked as invalid [_items enumerateObjectsUsingBlock:^(PSCollectionViewLayoutAttributes *itemAttributes, NSUInteger idx, BOOL *stop) { @@ -391,40 +324,79 @@ - (void)performLayout itemAttributes.currentColumn = shortestColumn; itemAttributes.alpha = 1.0f; itemAttributes.valid = YES; - itemAttributes.cell.frame = frame; //update the column heights _colHeights[shortestColumn] = @(colHeight + height + self.margin); + recalculateContentSize = YES; + + if (itemAttributes.previouslyVisible) { + [UIView animateWithDuration:kAnimationDuration animations:^{ + [self applyAttributes:itemAttributes toCell:itemAttributes.visibleCell]; + }]; + } else { + [self applyAttributes:itemAttributes toCell:itemAttributes.visibleCell]; + } } }]; - //TODO: update footer position + //Lays out items that are now visible and hides cells that are no longer visible + CGRect visibleRect = CGRectMake(self.contentOffset.x, self.contentOffset.y, self.width, self.height); + for (NSInteger i = 0; i < [_items count]; i++) { + PSCollectionViewLayoutAttributes *attributes = _items[i]; + if (CGRectIntersectsRect(visibleRect, attributes.frame) == NO) { + //Cell isn't visible, hide it + [self enqueueReusableView:attributes.visibleCell]; + attributes.visibleCell = nil; + + [_visibleItems removeIndex:i]; + } else if (attributes.visibleCell == nil) { + //Cell is now visible, add it in + PSCollectionViewCell *newCell = [self.collectionViewDataSource collectionView:self viewAtIndex:i]; + attributes.visibleCell = newCell; + [self addSubview:newCell]; + [_visibleItems addIndex:i]; + + if (self.animateLayoutChanges && attributes.previouslyVisible == NO) { + attributes.previouslyVisible = YES; + newCell.frame = attributes.frame; + newCell.alpha = 0.0f; + [UIView animateWithDuration:kAnimationDuration delay:0.0f options:UIViewAnimationOptionAllowUserInteraction animations:^{ + newCell.alpha = attributes.alpha; + } completion:nil]; + } else { + [self applyAttributes:attributes toCell:newCell]; + } + } + } - //update the content size - NSUInteger longestColumn = [self longestColumn]; - self.contentSize = CGSizeMake(self.width, [_colHeights[longestColumn] floatValue]); + //layout footer + CGSize footerSize = [self.footerView sizeThatFits:CGSizeMake(self.width, CGFLOAT_MAX)]; + if (self.footerView.height != footerSize.height) { + self.footerView.height = footerSize.height; + recalculateContentSize = YES; + } - _layoutInvalidated = NO; + //only update content size when it is needed + if (recalculateContentSize) { + [self updateContentSizeForColumnHeightChange]; + } } -- (void)insertItem +- (void)applyAttributes:(PSCollectionViewLayoutAttributes *)attributes toCell:(PSCollectionViewCell *)cell +{ + cell.frame = attributes.frame; + cell.alpha = attributes.alpha; +} + +- (void)insertItemAtEnd { [self insertItemAtIndex:[_items count]]; } - (void)insertItemAtIndex:(NSUInteger)index { - PSCollectionViewCell *cell = [self.collectionViewDataSource collectionView:self viewAtIndex:index]; PSCollectionViewLayoutAttributes *attributes = [[PSCollectionViewLayoutAttributes alloc] init]; - attributes.cell = cell; - - //if we are animating layout changes, fade in the cell - if (self.animateLayoutChanges) { - attributes.alpha = 0.0f; - } - [_items insertObject:attributes atIndex:index]; - [self addSubview:cell]; [self invalidateLayoutOfItemsAfterIndex:index]; @@ -437,14 +409,11 @@ - (void)insertItemAtIndex:(NSUInteger)index - (void)removeItemAtIndex:(NSUInteger)index { PSCollectionViewLayoutAttributes *attributes = _items[index]; - [self enqueueReusableView:attributes.cell]; + [self enqueueReusableView:attributes.visibleCell]; [_items removeObjectAtIndex:index]; [self invalidateLayoutOfItemsAfterIndex:index]; - //update the column heights - [self updateHeightOfColumnsAtIndex:index]; - //perform layout if not in a batch update if (_batchUpdateInProgress == NO) { [self performLayout]; @@ -454,27 +423,14 @@ - (void)removeItemAtIndex:(NSUInteger)index - (void)performBatchUpdates:(void (^)(void))updates completion:(void (^)(void))completion { _batchUpdateInProgress = YES; - [UIView animateWithDuration:kAnimationDuration animations:^{ - if (updates) { - updates(); - } - //perform layout to apply the changes - [self performLayout]; - } completion:^(BOOL finished) { - _batchUpdateInProgress = NO; - if (completion) { - completion(); - } - }]; -} - -- (void)updateHeightOfColumnsAtIndex:(NSUInteger)index -{ - //get the max Y values from the previous elements in each column (only need to get numCols number of elements) - //TODO: make sure this is iterating the correct number of times - for (int i = index; i > index - self.numCols && i >=0; i--) { - PSCollectionViewLayoutAttributes *attributes = _items[i]; - _colHeights[attributes.currentColumn] = @(CGRectGetMaxY(attributes.frame) + self.margin); + if (updates) { + updates(); + } + //perform layout to apply the changes + [self performLayout]; + _batchUpdateInProgress = NO; + if (completion) { + completion(); } } @@ -484,223 +440,40 @@ - (void)invalidateLayoutOfItemsAfterIndex:(NSUInteger)index PSCollectionViewLayoutAttributes *attributes = _items[i]; attributes.valid = NO; } -} - -#pragma mark - Old code after this in the section - -- (void)buildColumnOffsetsFromTop:(CGFloat) top -{ - self.colOffsets = [NSMutableArray arrayWithCapacity:self.numCols]; - for (int i = 0; i < self.numCols; i++) { - [_colOffsets addObject:[NSNumber numberWithFloat:top]]; - } -} - -- (NSInteger)findShortestColumn -{ - NSInteger col = 0; - CGFloat minHeight = [[_colOffsets objectAtIndex:col] floatValue]; - for (int i = 1; i < [_colOffsets count]; i++) { - CGFloat colHeight = [[_colOffsets objectAtIndex:i] floatValue]; - - if (colHeight < minHeight) { - col = i; - minHeight = colHeight; - } - } - return col; -} - -- (void)insertViewRectForIndex:(int)index forKey:(id )key inColumn:(NSInteger)col -{ - CGFloat left = self.margin + (col * self.margin) + (col * self.colWidth); - CGFloat top = [[_colOffsets objectAtIndex:col] floatValue]; - CGFloat colHeight = [self.collectionViewDataSource heightForViewAtIndex:index]; - if (colHeight == 0) { - colHeight = self.margin; - } - // Add to index rect map - CGRect viewRect = CGRectMake(left, top, self.colWidth, colHeight); - [self.indexToRectMap setObject:[NSValue valueWithCGRect:viewRect] forKey:key]; - - // Update the last height offset for this column - CGFloat test = top + colHeight + self.margin; - [_colOffsets replaceObjectAtIndex:col withObject:[NSNumber numberWithFloat:test]]; -} - -- (CGFloat)updateFooterViewWithTotalHeight:(CGFloat)totalHeight -{ - // Add footerView if exists - if (self.footerView) { - self.footerView.width = self.width; - self.footerView.top = totalHeight; + //get the max Y values from the previous elements in each column (only need to get numCols number of elements) + //this does not update the content size since that will be done in performLayout once all the batched changes are applied + //calculate until all columns have been updated + [self resetColumnHeights]; + NSMutableIndexSet *completedColumns = [NSMutableIndexSet indexSet]; + NSInteger i = index - 1; + while (i >= 0) { + PSCollectionViewLayoutAttributes *attributes = _items[i]; + if (attributes.valid && [completedColumns containsIndex:attributes.currentColumn] == NO) { + _colHeights[attributes.currentColumn] = @(CGRectGetMaxY(attributes.frame) + self.margin); + [completedColumns addIndex:attributes.currentColumn]; + } + //stop checking if all columns have been updated + if ([completedColumns count] == self.numCols) { + break; + } - CGSize footerSize = [self.footerView sizeThatFits:CGSizeMake(self.width, CGFLOAT_MAX)]; - self.footerView.height = footerSize.height; - totalHeight += self.footerView.height; - } - return totalHeight; + i--; + } } -- (CGFloat)totalHeightFromColOffsetsWithTotalHeight:(CGFloat)totalHeight +- (void)updateContentSizeForColumnHeightChange { - for (NSNumber *colHeight in _colOffsets) { - totalHeight = (totalHeight < [colHeight floatValue]) ? [colHeight floatValue] : totalHeight; - } - return totalHeight; -} - -- (void)relayoutViews { - self.numCols = UIInterfaceOrientationIsPortrait(self.orientation) ? self.numColsPortrait : self.numColsLandscape; - - // Reset all state - [self.visibleViews enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { - PSCollectionViewCell *view = (PSCollectionViewCell *)obj; - [self enqueueReusableView:view]; - }]; - [self.visibleViews removeAllObjects]; - [self.viewKeysToRemove removeAllObjects]; - [self.indexToRectMap removeAllObjects]; - if (_resetLoadedIndices) { - self.loadedIndices = [NSMutableIndexSet indexSet]; - _resetLoadedIndices = NO; - } + NSUInteger longestColumn = [self longestColumn]; + CGFloat longestColumnHeight = [_colHeights[longestColumn] floatValue]; - if (self.emptyView) { - [self.emptyView removeFromSuperview]; - } - [self.loadingView removeFromSuperview]; - - // This is where we should layout the entire grid first - NSInteger numViews = [self.collectionViewDataSource numberOfViewsInCollectionView:self]; - - CGFloat totalHeight = 0.0; - CGFloat top = self.margin; - - // Add headerView if it exists - if (self.headerView) { - self.headerView.width = self.width; + if (self.footerView) { + //position the footer view correctly + self.footerView.frame = CGRectMake(0, longestColumnHeight, self.width, self.footerView.height); - top = self.headerView.top; - [self addSubview:self.headerView]; - - CGSize headerSize = [self.headerView sizeThatFits:CGSizeMake(self.width, CGFLOAT_MAX)]; - self.headerView.height = headerSize.height; - self.headerViewHeight = headerSize.height; - top += self.headerView.height + self.margin; - } - - if (numViews > 0) { - // This array determines the last height offset on a column - [self buildColumnOffsetsFromTop:top]; - - // Calculate index to rect mapping - self.colWidth = floorf((self.width - self.margin * (self.numCols + 1)) / self.numCols); - for (NSInteger i = 0; i < numViews; i++) { - PSCollectionViewKey *key = PSCollectionKeyForIndex(i); - - // Find the shortest column - NSInteger col = [self findShortestColumn]; - [self insertViewRectForIndex:i forKey:key inColumn:col]; - } - - totalHeight = [self totalHeightFromColOffsetsWithTotalHeight:(CGFloat)totalHeight]; - } else { - totalHeight = self.height; - - // If we have an empty view, show it - if (self.emptyView) { - self.emptyView.frame = CGRectMake(self.margin, top, self.width - self.margin * 2, self.height - top - self.margin); - [self addSubview:self.emptyView]; - } else if (self.headerView) { - totalHeight = top; - } else { - totalHeight = top; - } - } - - totalHeight = [self updateFooterViewWithTotalHeight:totalHeight]; - - self.contentSize = CGSizeMake(self.width, totalHeight); - - [self removeAndAddCellsIfNecessary]; -} - -- (void)removeAndAddCellsIfNecessary { - static NSInteger bufferViewFactor = 5; - static NSInteger topIndex = 0; - static NSInteger bottomIndex = 0; - - NSInteger numViews = [self.collectionViewDataSource numberOfViewsInCollectionView:self]; - if (numViews == 0) { - return; + longestColumnHeight += self.footerView.frame.size.height; } - - // Find out what rows are visible - CGRect visibleRect = CGRectMake(self.contentOffset.x, self.contentOffset.y, self.width, self.height); - - // Remove all rows that are not inside the visible rect - [self.visibleViews enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { - PSCollectionViewCell *view = (PSCollectionViewCell *)obj; - CGRect viewRect = view.frame; - if (CGRectIntersectsRect(visibleRect, viewRect) == NO) { - [self enqueueReusableView:view]; - [self.viewKeysToRemove addObject:key]; - } - }]; - - [self.visibleViews removeObjectsForKeys:self.viewKeysToRemove]; - [self.viewKeysToRemove removeAllObjects]; - - if ([self.visibleViews count] == 0) { - topIndex = 0; - bottomIndex = numViews; - } else { - // need the highest and lowest values, so instead of an expensive sort, just iterate finding the high/low - NSArray *allKeys = [self.visibleViews allKeys]; - topIndex = [(PSCollectionViewKey*)[allKeys objectAtIndex:0] key]; - bottomIndex = [(PSCollectionViewKey*)[allKeys objectAtIndex:0] key]; - [allKeys enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { - NSInteger value = [(PSCollectionViewKey*)obj key]; - if (value < topIndex) { - topIndex = value; - } - if (value > bottomIndex) { - bottomIndex = value; - } - }]; - - topIndex = MAX(0, topIndex - (bufferViewFactor * self.numCols)); - bottomIndex = MIN(numViews, bottomIndex + (bufferViewFactor * self.numCols)); - } - - // Add views - for (NSInteger i = topIndex; i < bottomIndex; i++) { - PSCollectionViewKey *key = PSCollectionKeyForIndex(i); - CGRect rect = [[self.indexToRectMap objectForKey:key] CGRectValue]; - - // If view is within visible rect and is not already shown - if (![self.visibleViews objectForKey:key] && CGRectIntersectsRect(visibleRect, rect)) { - // Only add views if not visible - PSCollectionViewCell *newView = [self.collectionViewDataSource collectionView:self viewAtIndex:i]; - newView.frame = [[self.indexToRectMap objectForKey:key] CGRectValue]; - if ([self.loadedIndices containsIndex:i]) { - [self addSubview:newView]; - } else { //animate it in, add it to the set - [self.loadedIndices addIndex:i]; - [self addSubview:newView]; - if (self.animateLayoutChanges) { - newView.alpha = 0.0f; - [UIView animateWithDuration:kAnimationDuration delay:0.0f options:UIViewAnimationOptionAllowUserInteraction animations:^{ - newView.alpha = 1.0f; - } completion:nil]; - } - } - - [self.visibleViews setObject:newView forKey:key]; - } - } + self.contentSize = CGSizeMake(self.width, longestColumnHeight); } #pragma mark - Helpers @@ -737,20 +510,24 @@ - (NSUInteger)longestColumn - (PSCollectionViewCell *)dequeueReusableView { - PSCollectionViewCell *view = [self.reuseableViews anyObject]; + PSCollectionViewCell *view = [_reuseableViews anyObject]; if (view) { // Found a reusable view, remove it from the set - [self.reuseableViews removeObject:view]; + [_reuseableViews removeObject:view]; } return view; } - (void)enqueueReusableView:(PSCollectionViewCell *)view { + if (view == nil) { + return; + } + [view prepareForReuse]; view.frame = CGRectZero; view.alpha = 1.0f; - [self.reuseableViews addObject:view]; + [_reuseableViews addObject:view]; [view removeFromSuperview]; } @@ -760,39 +537,21 @@ - (void)didSelectView:(UITapGestureRecognizer *)gestureRecognizer { CGPoint tapPoint = [gestureRecognizer locationInView:self]; - //determine which grid item (if any) this tap was on - PSCollectionViewCell *viewCell = nil; - NSArray *visibleViewArray = [self.visibleViews allValues]; - for (PSCollectionViewCell *view in visibleViewArray) { - if (CGRectContainsPoint(view.frame, tapPoint)) { - viewCell = view; - break; + __block PSCollectionViewLayoutAttributes *selectedCell = nil; + __block NSUInteger selectedIndex = 0; + [_visibleItems enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { + PSCollectionViewLayoutAttributes *candidate = _items[idx]; + if (CGRectContainsPoint(candidate.frame, tapPoint)) { + selectedCell = candidate; + selectedIndex = idx; + *stop = YES; } - } + }]; - if (viewCell) { - NSValue *rectValue = [NSValue valueWithCGRect:viewCell.frame]; - NSArray *matchingKeys = [self.indexToRectMap allKeysForObject:rectValue]; - PSCollectionViewKey *key = [matchingKeys lastObject]; - if ([viewCell isMemberOfClass:[[self.visibleViews objectForKey:key] class]]) { - if (self.collectionViewDelegate && [self.collectionViewDelegate respondsToSelector:@selector(collectionView:didSelectView:atIndex:)]) { - NSInteger matchingIndex = PSCollectionIndexForKey(key); - [self.collectionViewDelegate collectionView:self didSelectView:viewCell atIndex:matchingIndex]; - } - } - } -} - -- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch -{ - if ([gestureRecognizer isMemberOfClass:[PSCollectionViewTapGestureRecognizer class]] == NO) { - return YES; + PSCollectionViewCell *cell = selectedCell.visibleCell; + if (cell) { + [self.collectionViewDelegate collectionView:self didSelectView:cell atIndex:selectedIndex]; } - - NSValue *rectValue = [NSValue valueWithCGRect:gestureRecognizer.view.frame]; - NSArray *matchingKeys = [self.indexToRectMap allKeysForObject:rectValue]; - NSString *key = [matchingKeys lastObject]; - return [touch.view isMemberOfClass:[[self.visibleViews objectForKey:key] class]]; } @end diff --git a/PSCollectionViewLayoutAttributes.h b/PSCollectionViewLayoutAttributes.h index d95a61d..befaef7 100644 --- a/PSCollectionViewLayoutAttributes.h +++ b/PSCollectionViewLayoutAttributes.h @@ -15,7 +15,8 @@ @property (nonatomic, assign) NSUInteger currentColumn; @property (nonatomic, assign) CGFloat alpha; @property (nonatomic, assign) BOOL valid; +@property (nonatomic, assign) BOOL previouslyVisible; -@property (nonatomic, strong) PSCollectionViewCell *cell; +@property (nonatomic, strong) PSCollectionViewCell *visibleCell; @end diff --git a/PSCollectionViewLayoutAttributes.m b/PSCollectionViewLayoutAttributes.m index f70d27b..bbd402e 100644 --- a/PSCollectionViewLayoutAttributes.m +++ b/PSCollectionViewLayoutAttributes.m @@ -18,6 +18,7 @@ - (id)init self.currentColumn = 0; self.alpha = 1.0f; self.valid = NO; + self.previouslyVisible = NO; } return self; } From ac33cdb63ebff62b84d4b66208211ea9572b221e Mon Sep 17 00:00:00 2001 From: Adam Becevello Date: Mon, 17 Dec 2012 17:07:27 -0500 Subject: [PATCH 34/55] Don't crash when resetting the view --- PSCollectionView.m | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/PSCollectionView.m b/PSCollectionView.m index d0e8e81..c06e4e4 100644 --- a/PSCollectionView.m +++ b/PSCollectionView.m @@ -227,10 +227,16 @@ - (void)setFooterView:(UIView *)footerView { [self invalidateLayout]; } -#pragma mark - DataSource +#pragma mark - Reset - (void)reloadData { + [_visibleItems removeAllIndexes]; + for (PSCollectionViewLayoutAttributes *attributes in _items) { + [self enqueueReusableView:attributes.visibleCell]; + } + [_items removeAllObjects]; + [self invalidateLayout]; } From 299895b562f0406599770f099be37915982d85ec Mon Sep 17 00:00:00 2001 From: Adam Becevello Date: Tue, 18 Dec 2012 10:43:56 -0500 Subject: [PATCH 35/55] Properly allow disabling of animations, and changing margins requires a layout invalidation --- PSCollectionView.m | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/PSCollectionView.m b/PSCollectionView.m index c06e4e4..dc50b39 100644 --- a/PSCollectionView.m +++ b/PSCollectionView.m @@ -178,6 +178,12 @@ - (void)setNumColsPortrait:(NSInteger)numColsPortrait [self invalidateLayout]; } +- (void)setMargin:(CGFloat)margin +{ + _margin = margin; + [self invalidateLayout]; +} + - (void)setLoadingView:(UIView *)loadingView { [_loadingView removeFromSuperview]; _loadingView = nil; @@ -335,7 +341,7 @@ - (void)performLayout _colHeights[shortestColumn] = @(colHeight + height + self.margin); recalculateContentSize = YES; - if (itemAttributes.previouslyVisible) { + if (self.animateLayoutChanges && itemAttributes.previouslyVisible) { [UIView animateWithDuration:kAnimationDuration animations:^{ [self applyAttributes:itemAttributes toCell:itemAttributes.visibleCell]; }]; @@ -364,7 +370,7 @@ - (void)performLayout if (self.animateLayoutChanges && attributes.previouslyVisible == NO) { attributes.previouslyVisible = YES; - newCell.frame = attributes.frame; + [self applyAttributes:attributes toCell:newCell]; newCell.alpha = 0.0f; [UIView animateWithDuration:kAnimationDuration delay:0.0f options:UIViewAnimationOptionAllowUserInteraction animations:^{ newCell.alpha = attributes.alpha; From 7a7c95bdd06b3cb4fe7bed51405fe41abf050a82 Mon Sep 17 00:00:00 2001 From: Adam Becevello Date: Tue, 18 Dec 2012 11:46:52 -0500 Subject: [PATCH 36/55] Resolve some issues with view selection on rotation and other rotation animation issues --- PSCollectionView.m | 54 +++++++++++++----------------- PSCollectionViewLayoutAttributes.h | 1 - PSCollectionViewLayoutAttributes.m | 1 - 3 files changed, 23 insertions(+), 33 deletions(-) diff --git a/PSCollectionView.m b/PSCollectionView.m index dc50b39..fae9a19 100644 --- a/PSCollectionView.m +++ b/PSCollectionView.m @@ -122,7 +122,6 @@ @implementation PSCollectionView { NSMutableSet *_reuseableViews; - NSMutableIndexSet *_visibleItems; NSMutableArray *_items; //position is by index, value is PSCollectionViewLayoutAttribute objects } @@ -144,7 +143,6 @@ - (id)initWithFrame:(CGRect)frame _orientation = [UIApplication sharedApplication].statusBarOrientation; _reuseableViews = [NSMutableSet set]; - _visibleItems = [NSMutableIndexSet indexSet]; _items = [NSMutableArray array]; PSCollectionViewTapGestureRecognizer *recognizer = [[PSCollectionViewTapGestureRecognizer alloc] initWithTarget:self action:@selector(didSelectView:)]; @@ -237,7 +235,6 @@ - (void)setFooterView:(UIView *)footerView { - (void)reloadData { - [_visibleItems removeAllIndexes]; for (PSCollectionViewLayoutAttributes *attributes in _items) { [self enqueueReusableView:attributes.visibleCell]; } @@ -250,7 +247,6 @@ - (void)reloadData - (void)invalidateLayout { - [_visibleItems removeAllIndexes]; [_items enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(PSCollectionViewLayoutAttributes *attributes, NSUInteger idx, BOOL *stop) { attributes.valid = NO; }]; @@ -334,19 +330,20 @@ - (void)performLayout CGRect frame = CGRectMake(colXOffset, colHeight, self.colWidth, height); itemAttributes.frame = frame; itemAttributes.currentColumn = shortestColumn; - itemAttributes.alpha = 1.0f; itemAttributes.valid = YES; //update the column heights _colHeights[shortestColumn] = @(colHeight + height + self.margin); recalculateContentSize = YES; - if (self.animateLayoutChanges && itemAttributes.previouslyVisible) { + if (self.animateLayoutChanges && itemAttributes.previouslyVisible && itemAttributes.visibleCell) { [UIView animateWithDuration:kAnimationDuration animations:^{ - [self applyAttributes:itemAttributes toCell:itemAttributes.visibleCell]; + itemAttributes.visibleCell.frame = itemAttributes.frame; }]; } else { - [self applyAttributes:itemAttributes toCell:itemAttributes.visibleCell]; + [UIView setAnimationsEnabled:NO]; + itemAttributes.visibleCell.frame = itemAttributes.frame; + [UIView setAnimationsEnabled:YES]; } } }]; @@ -354,29 +351,31 @@ - (void)performLayout //Lays out items that are now visible and hides cells that are no longer visible CGRect visibleRect = CGRectMake(self.contentOffset.x, self.contentOffset.y, self.width, self.height); for (NSInteger i = 0; i < [_items count]; i++) { - PSCollectionViewLayoutAttributes *attributes = _items[i]; - if (CGRectIntersectsRect(visibleRect, attributes.frame) == NO) { + PSCollectionViewLayoutAttributes *itemAttributes = _items[i]; + BOOL visibleCell = CGRectIntersectsRect(visibleRect, itemAttributes.frame); + if (visibleCell == NO && itemAttributes.visibleCell) { //Cell isn't visible, hide it - [self enqueueReusableView:attributes.visibleCell]; - attributes.visibleCell = nil; - - [_visibleItems removeIndex:i]; - } else if (attributes.visibleCell == nil) { + [self enqueueReusableView:itemAttributes.visibleCell]; + itemAttributes.visibleCell = nil; + } else if (visibleCell && itemAttributes.visibleCell == nil) { //Cell is now visible, add it in PSCollectionViewCell *newCell = [self.collectionViewDataSource collectionView:self viewAtIndex:i]; - attributes.visibleCell = newCell; + itemAttributes.visibleCell = newCell; [self addSubview:newCell]; - [_visibleItems addIndex:i]; - if (self.animateLayoutChanges && attributes.previouslyVisible == NO) { - attributes.previouslyVisible = YES; - [self applyAttributes:attributes toCell:newCell]; + if (self.animateLayoutChanges && itemAttributes.previouslyVisible == NO) { + itemAttributes.previouslyVisible = YES; + [UIView setAnimationsEnabled:NO]; + newCell.frame = itemAttributes.frame; newCell.alpha = 0.0f; + [UIView setAnimationsEnabled:YES]; [UIView animateWithDuration:kAnimationDuration delay:0.0f options:UIViewAnimationOptionAllowUserInteraction animations:^{ - newCell.alpha = attributes.alpha; + newCell.alpha = 1.0f; } completion:nil]; } else { - [self applyAttributes:attributes toCell:newCell]; + [UIView setAnimationsEnabled:NO]; + newCell.frame = itemAttributes.frame; + [UIView setAnimationsEnabled:YES]; } } } @@ -394,12 +393,6 @@ - (void)performLayout } } -- (void)applyAttributes:(PSCollectionViewLayoutAttributes *)attributes toCell:(PSCollectionViewCell *)cell -{ - cell.frame = attributes.frame; - cell.alpha = attributes.alpha; -} - - (void)insertItemAtEnd { [self insertItemAtIndex:[_items count]]; @@ -551,9 +544,8 @@ - (void)didSelectView:(UITapGestureRecognizer *)gestureRecognizer __block PSCollectionViewLayoutAttributes *selectedCell = nil; __block NSUInteger selectedIndex = 0; - [_visibleItems enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { - PSCollectionViewLayoutAttributes *candidate = _items[idx]; - if (CGRectContainsPoint(candidate.frame, tapPoint)) { + [_items enumerateObjectsUsingBlock:^(PSCollectionViewLayoutAttributes *candidate, NSUInteger idx, BOOL *stop) { + if (candidate.valid && CGRectContainsPoint(candidate.frame, tapPoint)) { selectedCell = candidate; selectedIndex = idx; *stop = YES; diff --git a/PSCollectionViewLayoutAttributes.h b/PSCollectionViewLayoutAttributes.h index befaef7..0db4c96 100644 --- a/PSCollectionViewLayoutAttributes.h +++ b/PSCollectionViewLayoutAttributes.h @@ -13,7 +13,6 @@ @property (nonatomic, assign) CGRect frame; @property (nonatomic, assign) NSUInteger currentColumn; -@property (nonatomic, assign) CGFloat alpha; @property (nonatomic, assign) BOOL valid; @property (nonatomic, assign) BOOL previouslyVisible; diff --git a/PSCollectionViewLayoutAttributes.m b/PSCollectionViewLayoutAttributes.m index bbd402e..1efa726 100644 --- a/PSCollectionViewLayoutAttributes.m +++ b/PSCollectionViewLayoutAttributes.m @@ -16,7 +16,6 @@ - (id)init if (self) { self.frame = CGRectZero; self.currentColumn = 0; - self.alpha = 1.0f; self.valid = NO; self.previouslyVisible = NO; } From ad80391a0509ea53e915b3597bdd2c48e658cd97 Mon Sep 17 00:00:00 2001 From: Adam Becevello Date: Wed, 19 Dec 2012 09:46:46 -0500 Subject: [PATCH 37/55] The height of each cell needs to be recalculated when the entire layout is invalidated --- PSCollectionView.m | 1 + 1 file changed, 1 insertion(+) diff --git a/PSCollectionView.m b/PSCollectionView.m index fae9a19..f2384bd 100644 --- a/PSCollectionView.m +++ b/PSCollectionView.m @@ -249,6 +249,7 @@ - (void)invalidateLayout { [_items enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(PSCollectionViewLayoutAttributes *attributes, NSUInteger idx, BOOL *stop) { attributes.valid = NO; + attributes.frame = CGRectZero; }]; self.numCols = UIInterfaceOrientationIsPortrait(_orientation) ? self.numColsPortrait : self.numColsLandscape; From 7106b9c618e6946f2df0959159c252f107221c80 Mon Sep 17 00:00:00 2001 From: Adam Becevello Date: Wed, 19 Dec 2012 10:05:15 -0500 Subject: [PATCH 38/55] Properly handle reloadData --- PSCollectionView.m | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/PSCollectionView.m b/PSCollectionView.m index f2384bd..53b3f11 100644 --- a/PSCollectionView.m +++ b/PSCollectionView.m @@ -238,9 +238,16 @@ - (void)reloadData for (PSCollectionViewLayoutAttributes *attributes in _items) { [self enqueueReusableView:attributes.visibleCell]; } - [_items removeAllObjects]; + _items = [NSMutableArray array]; + + //recreate items for the number of views that will appear in the grid + NSInteger numCells = [self.collectionViewDataSource numberOfViewsInCollectionView:self]; + for (NSInteger i = 0; i < numCells; i++) { + [_items addObject:[[PSCollectionViewLayoutAttributes alloc] init]]; + } [self invalidateLayout]; + [self setNeedsLayout]; } #pragma mark - View From a0b75c8d6bccf21c72b5cbfd93e4260ae6a98b1d Mon Sep 17 00:00:00 2001 From: Adam Becevello Date: Wed, 19 Dec 2012 11:57:08 -0500 Subject: [PATCH 39/55] Fully support reuse identifiers --- PSCollectionView.h | 2 +- PSCollectionView.m | 33 +++++++++++++++++++++++---------- PSCollectionViewCell.h | 3 +++ PSCollectionViewCell.m | 29 +++++++++++++---------------- 4 files changed, 40 insertions(+), 27 deletions(-) diff --git a/PSCollectionView.h b/PSCollectionView.h index d52d741..8ec3633 100644 --- a/PSCollectionView.h +++ b/PSCollectionView.h @@ -59,7 +59,7 @@ Dequeues a reusable view that was previously initialized This is similar to UITableView dequeueReusableCellWithIdentifier */ -- (UIView *)dequeueReusableView; +- (PSCollectionViewCell *)dequeueReusableViewWithIdentifier:(NSString *)reuseIdentifier; - (void)insertItemAtEnd; - (void)insertItemAtIndex:(NSUInteger)index; diff --git a/PSCollectionView.m b/PSCollectionView.m index 53b3f11..a203230 100644 --- a/PSCollectionView.m +++ b/PSCollectionView.m @@ -120,7 +120,7 @@ @implementation PSCollectionView { NSMutableArray *_colXOffsets; NSMutableArray *_colHeights; - NSMutableSet *_reuseableViews; + NSMutableDictionary *_reusableViews; NSMutableArray *_items; //position is by index, value is PSCollectionViewLayoutAttribute objects } @@ -142,7 +142,7 @@ - (id)initWithFrame:(CGRect)frame self.animateLayoutChanges = YES; _orientation = [UIApplication sharedApplication].statusBarOrientation; - _reuseableViews = [NSMutableSet set]; + _reusableViews = [NSMutableDictionary dictionary]; _items = [NSMutableArray array]; PSCollectionViewTapGestureRecognizer *recognizer = [[PSCollectionViewTapGestureRecognizer alloc] initWithTarget:self action:@selector(didSelectView:)]; @@ -521,14 +521,22 @@ - (NSUInteger)longestColumn #pragma mark - Reusing Views -- (PSCollectionViewCell *)dequeueReusableView +- (PSCollectionViewCell *)dequeueReusableViewWithIdentifier:(NSString *)reuseIdentifier { - PSCollectionViewCell *view = [_reuseableViews anyObject]; - if (view) { - // Found a reusable view, remove it from the set - [_reuseableViews removeObject:view]; - } - return view; + if ([reuseIdentifier length] == 0) { + return nil; + } + + NSMutableSet *reusableViewsForIdentifier = [_reusableViews objectForKey:reuseIdentifier]; + if (reusableViewsForIdentifier) { + PSCollectionViewCell *view = [reusableViewsForIdentifier anyObject]; + if (view) { + // Found a reusable view, remove it from the set + [reusableViewsForIdentifier removeObject:view]; + return view; + } + } + return nil; } - (void)enqueueReusableView:(PSCollectionViewCell *)view @@ -540,7 +548,12 @@ - (void)enqueueReusableView:(PSCollectionViewCell *)view [view prepareForReuse]; view.frame = CGRectZero; view.alpha = 1.0f; - [_reuseableViews addObject:view]; + + NSMutableSet *reusableViewsForIdentifier = [_reusableViews objectForKey:view.reuseIdentifier]; + if (reusableViewsForIdentifier == nil && [view.reuseIdentifier length] > 0) { + [_reusableViews setObject:[NSMutableSet set] forKey:view.reuseIdentifier]; + } + [reusableViewsForIdentifier addObject:view]; [view removeFromSuperview]; } diff --git a/PSCollectionViewCell.h b/PSCollectionViewCell.h index c9ba811..24c50a3 100644 --- a/PSCollectionViewCell.h +++ b/PSCollectionViewCell.h @@ -26,6 +26,9 @@ @interface PSCollectionViewCell : UIView @property (nonatomic, strong) id object; +@property (nonatomic, readonly, copy) NSString *reuseIdentifier; + +- (id)initWithFrame:(CGRect)frame reuseIdentifier:(NSString *)reuseIdentifier; - (void)prepareForReuse; - (void)fillViewWithObject:(id)object; diff --git a/PSCollectionViewCell.m b/PSCollectionViewCell.m index 435ebf6..118dc80 100644 --- a/PSCollectionViewCell.m +++ b/PSCollectionViewCell.m @@ -23,32 +23,29 @@ #import "PSCollectionViewCell.h" -@interface PSCollectionViewCell () - -@end - @implementation PSCollectionViewCell -@synthesize -object = _object; - -- (id)initWithFrame:(CGRect)frame { - self = [super initWithFrame:frame]; - if (self) { - } - return self; +- (id)initWithFrame:(CGRect)frame reuseIdentifier:(NSString *)reuseIdentifier +{ + self = [super initWithFrame:frame]; + if (self) { + _reuseIdentifier = [reuseIdentifier copy]; + } + return self; } - -- (void)prepareForReuse { +- (void)prepareForReuse +{ self.object = nil; } -- (void)fillViewWithObject:(id)object { +- (void)fillViewWithObject:(id)object +{ self.object = object; } -+ (CGFloat)heightForViewWithObject:(id)object inColumnWidth:(CGFloat)columnWidth { ++ (CGFloat)heightForViewWithObject:(id)object inColumnWidth:(CGFloat)columnWidth +{ return 0.0; } From 5587b28c921caef4d654c5d28bcfbf4273db8882 Mon Sep 17 00:00:00 2001 From: Adam Becevello Date: Thu, 20 Dec 2012 10:57:24 -0500 Subject: [PATCH 40/55] Code to support sections --- PSCollectionView.h | 14 +- PSCollectionView.m | 376 +++++++++++++++++++++++++++++---------------- 2 files changed, 253 insertions(+), 137 deletions(-) diff --git a/PSCollectionView.h b/PSCollectionView.h index 8ec3633..94860b8 100644 --- a/PSCollectionView.h +++ b/PSCollectionView.h @@ -61,9 +61,8 @@ */ - (PSCollectionViewCell *)dequeueReusableViewWithIdentifier:(NSString *)reuseIdentifier; -- (void)insertItemAtEnd; -- (void)insertItemAtIndex:(NSUInteger)index; -- (void)removeItemAtIndex:(NSUInteger)index; +- (void)insertItemAtIndexPath:(NSIndexPath *)indexPath; +- (void)removeItemAtIndexPath:(NSIndexPath *)indexPath; - (void)performBatchUpdates:(void (^)(void))updates completion:(void (^)(void))completion; @@ -74,7 +73,7 @@ @protocol PSCollectionViewDelegate @optional -- (void)collectionView:(PSCollectionView *)collectionView didSelectView:(PSCollectionViewCell *)view atIndex:(NSInteger)index; +- (void)collectionView:(PSCollectionView *)collectionView didSelectView:(PSCollectionViewCell *)view atIndexPath:(NSIndexPath *)indexPath; @end @@ -83,8 +82,9 @@ @protocol PSCollectionViewDataSource @required -- (NSInteger)numberOfViewsInCollectionView:(PSCollectionView *)collectionView; -- (PSCollectionViewCell *)collectionView:(PSCollectionView *)collectionView viewAtIndex:(NSInteger)index; -- (CGFloat)heightForViewAtIndex:(NSInteger)index; +- (NSUInteger)numberOfSectionsInCollectionView:(PSCollectionView *)collectionView; +- (NSUInteger)collectionView:(PSCollectionView *)collectionView numberOfViewsInSection:(NSUInteger)section; +- (PSCollectionViewCell *)collectionView:(PSCollectionView *)collectionView viewAtIndexPath:(NSIndexPath *)indexPath; +- (CGFloat)heightForViewAtIndexPath:(NSIndexPath *)indexPath; @end diff --git a/PSCollectionView.m b/PSCollectionView.m index a203230..673d29e 100644 --- a/PSCollectionView.m +++ b/PSCollectionView.m @@ -115,14 +115,17 @@ @interface PSCollectionView () @implementation PSCollectionView { BOOL _batchUpdateInProgress; + NSInteger _numSections; + UIInterfaceOrientation _orientation; NSMutableArray *_colXOffsets; - NSMutableArray *_colHeights; + + NSMutableArray *_sectionColumnHeights; //contains an array of arrays, first array is positioned by section number, values are the heights of each column within each section NSMutableDictionary *_reusableViews; - NSMutableArray *_items; //position is by index, value is PSCollectionViewLayoutAttribute objects + NSMutableDictionary *_sectionItems; //key is section number, value is array with position is by index, value is PSCollectionViewLayoutAttribute objects } #pragma mark - Init/Memory @@ -141,9 +144,10 @@ - (id)initWithFrame:(CGRect)frame self.numColsLandscape = 0; self.animateLayoutChanges = YES; + _numSections = 1; _orientation = [UIApplication sharedApplication].statusBarOrientation; _reusableViews = [NSMutableDictionary dictionary]; - _items = [NSMutableArray array]; + _sectionItems = [NSMutableDictionary dictionary]; PSCollectionViewTapGestureRecognizer *recognizer = [[PSCollectionViewTapGestureRecognizer alloc] initWithTarget:self action:@selector(didSelectView:)]; [recognizer setCancelsTouchesInView:NO]; @@ -231,19 +235,33 @@ - (void)setFooterView:(UIView *)footerView { [self invalidateLayout]; } +- (void)setFrame:(CGRect)frame +{ + [super setFrame:frame]; + [self invalidateLayout]; +} + #pragma mark - Reset - (void)reloadData { - for (PSCollectionViewLayoutAttributes *attributes in _items) { - [self enqueueReusableView:attributes.visibleCell]; + for (NSArray *sectionItems in [_sectionItems allValues]) { + for (PSCollectionViewLayoutAttributes *attributes in sectionItems) { + [self enqueueReusableView:attributes.visibleCell]; + } } - _items = [NSMutableArray array]; - //recreate items for the number of views that will appear in the grid - NSInteger numCells = [self.collectionViewDataSource numberOfViewsInCollectionView:self]; - for (NSInteger i = 0; i < numCells; i++) { - [_items addObject:[[PSCollectionViewLayoutAttributes alloc] init]]; + _sectionItems = [NSMutableDictionary dictionary]; + + _numSections = [self.collectionViewDataSource numberOfSectionsInCollectionView:self]; + for (NSUInteger i = 0; i < _numSections; i++) { + //recreate items for the number of views that will appear in the grid + NSInteger numCells = [self.collectionViewDataSource collectionView:self numberOfViewsInSection:i]; + NSMutableArray *items = [NSMutableArray arrayWithCapacity:numCells]; + for (NSUInteger i = 0; i < numCells; i++) { + [items addObject:[[PSCollectionViewLayoutAttributes alloc] init]]; + } + [_sectionItems setObject:items forKey:@(i)]; } [self invalidateLayout]; @@ -252,12 +270,19 @@ - (void)reloadData #pragma mark - View +- (void)invalidateItemLayoutAttributes:(PSCollectionViewLayoutAttributes *)attributes +{ + attributes.valid = NO; + attributes.frame = CGRectZero; +} + - (void)invalidateLayout { - [_items enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(PSCollectionViewLayoutAttributes *attributes, NSUInteger idx, BOOL *stop) { - attributes.valid = NO; - attributes.frame = CGRectZero; - }]; + for (NSArray *sectionItems in [_sectionItems allValues]) { + [sectionItems enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(PSCollectionViewLayoutAttributes *attributes, NSUInteger idx, BOOL *stop) { + [self invalidateItemLayoutAttributes:attributes]; + }]; + } self.numCols = UIInterfaceOrientationIsPortrait(_orientation) ? self.numColsPortrait : self.numColsLandscape; @@ -265,17 +290,34 @@ - (void)invalidateLayout _colXOffsets = nil; [self resetColumnHeights]; - self.colWidth = floorf((self.width - self.margin * (self.numCols + 1)) / self.numCols); + if (self.numCols == 0) { + self.colWidth = 0.0f; + } else { + self.colWidth = floorf((self.width - self.margin * (self.numCols + 1)) / self.numCols); + } } - (void)resetColumnHeights { - _colHeights = [NSMutableArray arrayWithCapacity:self.numCols]; - for (int i = 0; i < self.numCols; i++) { - [_colHeights addObject:@(self.headerView.height + self.margin)]; + //sections are assumed to be the same height, this will change as items are added to each sections + _sectionColumnHeights = [NSMutableArray array]; + for (NSUInteger i = 0; i < _numSections; i++) { + [_sectionColumnHeights addObject:@0.0f]; //ensure the array has a position for this section, this simplifies the resetColumnHeightsInSection method + [self resetColumnHeightsInSection:i]; } } +- (void)resetColumnHeightsInSection:(NSUInteger)section +{ + //sections are assumed to be the same height, this will change as items are added to each sections + NSNumber *marginHeight = @(self.margin); + NSMutableArray *colHeights = [NSMutableArray array]; + for (NSUInteger i = 0; i < self.numCols; i++) { + [colHeights addObject:marginHeight]; + } + _sectionColumnHeights[section] = colHeights; +} + - (void)layoutSubviews { [super layoutSubviews]; @@ -305,11 +347,19 @@ - (void)performLayout } //handle displaying and hiding the empty view - if ([_items count] == 0 && self.emptyView) { - self.emptyView.frame = CGRectMake(self.margin, self.margin, self.width - self.margin * 2, self.height - self.margin * 2); - self.emptyView.hidden = NO; - } else { - self.emptyView.hidden = YES; + if (self.emptyView) { + BOOL hasItems = NO; + for (NSArray *sectonItems in [_sectionItems allValues]) { + if ([sectonItems count] > 0) { + hasItems = YES; + } + } + if (hasItems == NO) { + self.emptyView.frame = CGRectMake(self.margin, self.margin, self.width - self.margin * 2, self.height - self.margin * 2); + self.emptyView.hidden = NO; + } else { + self.emptyView.hidden = YES; + } } if (_colXOffsets == nil) { @@ -323,70 +373,79 @@ - (void)performLayout //calculate the positions of all cells, but skip the cells that had no change //a cell has a change if its layout is marked as invalid - [_items enumerateObjectsUsingBlock:^(PSCollectionViewLayoutAttributes *itemAttributes, NSUInteger idx, BOOL *stop) { - if (itemAttributes.valid == NO) { - //ensure we have the height for this item - CGFloat height = itemAttributes.frame.size.height; - if (height == 0.0f) { - height = [self.collectionViewDataSource heightForViewAtIndex:idx]; - } - - //find the shortest column - NSUInteger shortestColumn = [self shortestColumn]; - CGFloat colXOffset = [_colXOffsets[shortestColumn] floatValue]; - CGFloat colHeight = [_colHeights[shortestColumn] floatValue]; - CGRect frame = CGRectMake(colXOffset, colHeight, self.colWidth, height); - itemAttributes.frame = frame; - itemAttributes.currentColumn = shortestColumn; - itemAttributes.valid = YES; - - //update the column heights - _colHeights[shortestColumn] = @(colHeight + height + self.margin); - recalculateContentSize = YES; - - if (self.animateLayoutChanges && itemAttributes.previouslyVisible && itemAttributes.visibleCell) { - [UIView animateWithDuration:kAnimationDuration animations:^{ + [_sectionItems enumerateKeysAndObjectsUsingBlock:^(NSNumber *sectionNumber, NSArray *sectionItems, BOOL *stop) { + NSUInteger section = [sectionNumber integerValue]; + [sectionItems enumerateObjectsUsingBlock:^(PSCollectionViewLayoutAttributes *itemAttributes, NSUInteger idx, BOOL *stop) { + if (itemAttributes.valid == NO) { + NSIndexPath *indexPath = [NSIndexPath indexPathForItem:idx inSection:section]; + + //ensure we have the height for this item + CGFloat height = itemAttributes.frame.size.height; + if (height == 0.0f) { + height = [self.collectionViewDataSource heightForViewAtIndexPath:indexPath]; + } + + //find the shortest column + NSUInteger shortestColumn = [self shortestColumnInSection:section]; + CGFloat colXOffset = [_colXOffsets[shortestColumn] floatValue]; + CGFloat yOffset = [self yOffsetForItemInSection:section column:shortestColumn]; + CGRect frame = CGRectMake(colXOffset, yOffset, self.colWidth, height); + itemAttributes.frame = frame; + itemAttributes.currentColumn = shortestColumn; + itemAttributes.valid = YES; + + //update the column heights + [self updateHeightOfColumn:shortestColumn inSection:section withAdditionalHeight:height + self.margin]; + recalculateContentSize = YES; + + if (self.animateLayoutChanges && itemAttributes.previouslyVisible && itemAttributes.visibleCell) { + [UIView animateWithDuration:kAnimationDuration animations:^{ + itemAttributes.visibleCell.frame = itemAttributes.frame; + }]; + } else { + [UIView setAnimationsEnabled:NO]; itemAttributes.visibleCell.frame = itemAttributes.frame; - }]; - } else { - [UIView setAnimationsEnabled:NO]; - itemAttributes.visibleCell.frame = itemAttributes.frame; - [UIView setAnimationsEnabled:YES]; + [UIView setAnimationsEnabled:YES]; + } } - } + }]; }]; + //Lays out items that are now visible and hides cells that are no longer visible CGRect visibleRect = CGRectMake(self.contentOffset.x, self.contentOffset.y, self.width, self.height); - for (NSInteger i = 0; i < [_items count]; i++) { - PSCollectionViewLayoutAttributes *itemAttributes = _items[i]; - BOOL visibleCell = CGRectIntersectsRect(visibleRect, itemAttributes.frame); - if (visibleCell == NO && itemAttributes.visibleCell) { - //Cell isn't visible, hide it - [self enqueueReusableView:itemAttributes.visibleCell]; - itemAttributes.visibleCell = nil; - } else if (visibleCell && itemAttributes.visibleCell == nil) { - //Cell is now visible, add it in - PSCollectionViewCell *newCell = [self.collectionViewDataSource collectionView:self viewAtIndex:i]; - itemAttributes.visibleCell = newCell; - [self addSubview:newCell]; - - if (self.animateLayoutChanges && itemAttributes.previouslyVisible == NO) { - itemAttributes.previouslyVisible = YES; - [UIView setAnimationsEnabled:NO]; - newCell.frame = itemAttributes.frame; - newCell.alpha = 0.0f; - [UIView setAnimationsEnabled:YES]; - [UIView animateWithDuration:kAnimationDuration delay:0.0f options:UIViewAnimationOptionAllowUserInteraction animations:^{ - newCell.alpha = 1.0f; - } completion:nil]; - } else { - [UIView setAnimationsEnabled:NO]; - newCell.frame = itemAttributes.frame; - [UIView setAnimationsEnabled:YES]; + [_sectionItems enumerateKeysAndObjectsUsingBlock:^(NSNumber *sectionNumber, NSArray *sectionItems, BOOL *stop) { + for (NSUInteger i = 0; i < [sectionItems count]; i++) { + PSCollectionViewLayoutAttributes *itemAttributes = sectionItems[i]; + BOOL visibleCell = CGRectIntersectsRect(visibleRect, itemAttributes.frame); + if (visibleCell == NO && itemAttributes.visibleCell) { + //Cell isn't visible, hide it + [self enqueueReusableView:itemAttributes.visibleCell]; + itemAttributes.visibleCell = nil; + } else if (visibleCell && itemAttributes.visibleCell == nil) { + //Cell is now visible, add it in + NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:[sectionNumber integerValue]]; + PSCollectionViewCell *newCell = [self.collectionViewDataSource collectionView:self viewAtIndexPath:indexPath]; + itemAttributes.visibleCell = newCell; + [self addSubview:newCell]; + + if (self.animateLayoutChanges && itemAttributes.previouslyVisible == NO) { + itemAttributes.previouslyVisible = YES; + [UIView setAnimationsEnabled:NO]; + newCell.frame = itemAttributes.frame; + newCell.alpha = 0.0f; + [UIView setAnimationsEnabled:YES]; + [UIView animateWithDuration:kAnimationDuration delay:0.0f options:UIViewAnimationOptionAllowUserInteraction animations:^{ + newCell.alpha = 1.0f; + } completion:nil]; + } else { + [UIView setAnimationsEnabled:NO]; + newCell.frame = itemAttributes.frame; + [UIView setAnimationsEnabled:YES]; + } } } - } + }]; //layout footer CGSize footerSize = [self.footerView sizeThatFits:CGSizeMake(self.width, CGFLOAT_MAX)]; @@ -401,17 +460,19 @@ - (void)performLayout } } -- (void)insertItemAtEnd -{ - [self insertItemAtIndex:[_items count]]; -} - -- (void)insertItemAtIndex:(NSUInteger)index +- (void)insertItemAtIndexPath:(NSIndexPath *)indexPath { PSCollectionViewLayoutAttributes *attributes = [[PSCollectionViewLayoutAttributes alloc] init]; - [_items insertObject:attributes atIndex:index]; - [self invalidateLayoutOfItemsAfterIndex:index]; + NSNumber *section = @(indexPath.section); + NSMutableArray *sectionItems = [_sectionItems objectForKey:section]; + if (sectionItems == nil) { + sectionItems = [NSMutableArray array]; + [_sectionItems setObject:sectionItems forKey:section]; + } + [sectionItems insertObject:attributes atIndex:indexPath.item]; + + [self invalidateLayoutOfItemsAfterIndexPath:indexPath]; //erform layout if not in a batch update if (_batchUpdateInProgress == NO) { @@ -419,17 +480,21 @@ - (void)insertItemAtIndex:(NSUInteger)index } } -- (void)removeItemAtIndex:(NSUInteger)index +- (void)removeItemAtIndexPath:(NSIndexPath *)indexPath { - PSCollectionViewLayoutAttributes *attributes = _items[index]; - [self enqueueReusableView:attributes.visibleCell]; - - [_items removeObjectAtIndex:index]; - [self invalidateLayoutOfItemsAfterIndex:index]; - - //perform layout if not in a batch update - if (_batchUpdateInProgress == NO) { - [self performLayout]; + NSNumber *section = @(indexPath.section); + NSMutableArray *sectionItems = [_sectionItems objectForKey:section]; + if (sectionItems) { + PSCollectionViewLayoutAttributes *attributes = sectionItems[indexPath.item]; + [self enqueueReusableView:attributes.visibleCell]; + + [sectionItems removeObjectAtIndex:indexPath.item]; + [self invalidateLayoutOfItemsAfterIndexPath:indexPath]; + + //perform layout if not in a batch update + if (_batchUpdateInProgress == NO) { + [self performLayout]; + } } } @@ -447,23 +512,37 @@ - (void)performBatchUpdates:(void (^)(void))updates completion:(void (^)(void))c } } -- (void)invalidateLayoutOfItemsAfterIndex:(NSUInteger)index +- (void)invalidateLayoutOfItemsAfterIndexPath:(NSIndexPath *)indexPath { - for (int i=index; i < [_items count]; i++) { - PSCollectionViewLayoutAttributes *attributes = _items[i]; - attributes.valid = NO; - } + [_sectionItems enumerateKeysAndObjectsUsingBlock:^(NSNumber *sectionNumber, NSMutableArray *sectionItems, BOOL *stop) { + NSInteger section = [sectionNumber integerValue]; + if (indexPath.section > section) { + //invalidate all items in this section + for (PSCollectionViewLayoutAttributes *attributes in sectionItems) { + [self invalidateItemLayoutAttributes:attributes]; + } + [self resetColumnHeightsInSection:section]; + } else if (indexPath.section == section) { + //invalidate only items after this item + for (int i=indexPath.item; i < [sectionItems count]; i++) { + PSCollectionViewLayoutAttributes *attributes = sectionItems[i]; + [self invalidateItemLayoutAttributes:attributes]; + } + [self resetColumnHeightsInSection:section]; + } + }]; //get the max Y values from the previous elements in each column (only need to get numCols number of elements) //this does not update the content size since that will be done in performLayout once all the batched changes are applied //calculate until all columns have been updated - [self resetColumnHeights]; NSMutableIndexSet *completedColumns = [NSMutableIndexSet indexSet]; - NSInteger i = index - 1; + NSInteger i = indexPath.item - 1; + NSMutableArray *sectionItems = [_sectionItems objectForKey:@(indexPath.section)]; while (i >= 0) { - PSCollectionViewLayoutAttributes *attributes = _items[i]; + PSCollectionViewLayoutAttributes *attributes = sectionItems[i]; if (attributes.valid && [completedColumns containsIndex:attributes.currentColumn] == NO) { - _colHeights[attributes.currentColumn] = @(CGRectGetMaxY(attributes.frame) + self.margin); + NSMutableArray *colHeights = _sectionColumnHeights[indexPath.section]; + colHeights[attributes.currentColumn] = @(CGRectGetMaxY(attributes.frame) - self.headerView.height + self.margin); [completedColumns addIndex:attributes.currentColumn]; } //stop checking if all columns have been updated @@ -477,26 +556,62 @@ - (void)invalidateLayoutOfItemsAfterIndex:(NSUInteger)index - (void)updateContentSizeForColumnHeightChange { - NSUInteger longestColumn = [self longestColumn]; - CGFloat longestColumnHeight = [_colHeights[longestColumn] floatValue]; + //calculate the height of all combined sections + CGFloat totalHeight = 0.0f; + + if (self.headerView) { + totalHeight += self.headerView.height; + } + + for (NSUInteger i = 0; i < _numSections; i++) { + totalHeight += [self heightOfLongestColumnInSection:i]; + } if (self.footerView) { //position the footer view correctly - self.footerView.frame = CGRectMake(0, longestColumnHeight, self.width, self.footerView.height); + self.footerView.frame = CGRectMake(0, totalHeight, self.width, self.footerView.height); - longestColumnHeight += self.footerView.frame.size.height; + totalHeight += self.footerView.frame.size.height; } - self.contentSize = CGSizeMake(self.width, longestColumnHeight); + self.contentSize = CGSizeMake(self.width, totalHeight); } #pragma mark - Helpers -- (NSUInteger)shortestColumn +- (CGFloat)yOffsetForItemInSection:(NSUInteger)section column:(NSUInteger)column +{ + //the yoffset is the height of all previous sections, plus the height of the column in the current section + CGFloat height = 0.0f; + + if (self.headerView) { + height += self.headerView.height; + } + + if (section > 0) { + for (NSUInteger i = 0; i < section - 1; i++) { + height += [self heightOfLongestColumnInSection:i]; + } + } + + NSArray *columnHeights = _sectionColumnHeights[section]; + height += [columnHeights[column] floatValue]; + return height; +} + +- (void)updateHeightOfColumn:(NSInteger)column inSection:(NSUInteger)section withAdditionalHeight:(CGFloat)height +{ + NSMutableArray *columnHeights = _sectionColumnHeights[section]; + NSNumber *currentHeight = columnHeights[column]; + columnHeights[column] = @([currentHeight floatValue] + height); +} + +- (NSUInteger)shortestColumnInSection:(NSUInteger)section { NSInteger col = 0; - CGFloat minHeight = [_colHeights[col] floatValue]; - for (int i = 1; i < [_colHeights count]; i++) { - CGFloat colHeight = [_colHeights[i] floatValue]; + NSMutableArray *sectionColumnHeights = _sectionColumnHeights[section]; + CGFloat minHeight = [sectionColumnHeights[col] floatValue]; + for (int i = 1; i < [sectionColumnHeights count]; i++) { + CGFloat colHeight = [sectionColumnHeights[i] floatValue]; if (colHeight < minHeight) { col = i; minHeight = colHeight; @@ -505,18 +620,17 @@ - (NSUInteger)shortestColumn return col; } -- (NSUInteger)longestColumn +- (CGFloat)heightOfLongestColumnInSection:(NSUInteger)section { - NSInteger col = 0; - CGFloat maxHeight = [_colHeights[col] floatValue]; - for (int i = 1; i < [_colHeights count]; i++) { - CGFloat colHeight = [_colHeights[i] floatValue]; + NSMutableArray *sectionColumnHeights = _sectionColumnHeights[section]; + CGFloat maxHeight = [sectionColumnHeights[0] floatValue]; + for (int i = 1; i < [sectionColumnHeights count]; i++) { + CGFloat colHeight = [sectionColumnHeights[i] floatValue]; if (colHeight > maxHeight) { - col = i; maxHeight = colHeight; } } - return col; + return maxHeight; } #pragma mark - Reusing Views @@ -564,18 +678,20 @@ - (void)didSelectView:(UITapGestureRecognizer *)gestureRecognizer CGPoint tapPoint = [gestureRecognizer locationInView:self]; __block PSCollectionViewLayoutAttributes *selectedCell = nil; - __block NSUInteger selectedIndex = 0; - [_items enumerateObjectsUsingBlock:^(PSCollectionViewLayoutAttributes *candidate, NSUInteger idx, BOOL *stop) { - if (candidate.valid && CGRectContainsPoint(candidate.frame, tapPoint)) { - selectedCell = candidate; - selectedIndex = idx; - *stop = YES; - } + __block NSIndexPath *selectedIndexPath = nil; + [_sectionItems enumerateKeysAndObjectsUsingBlock:^(NSNumber *sectionNumber, NSArray *sectionItems, BOOL *stop) { + [sectionItems enumerateObjectsUsingBlock:^(PSCollectionViewLayoutAttributes *candidate, NSUInteger idx, BOOL *stop) { + if (candidate.valid && CGRectContainsPoint(candidate.frame, tapPoint)) { + selectedCell = candidate; + selectedIndexPath = [NSIndexPath indexPathForItem:idx inSection:[sectionNumber integerValue]]; + *stop = YES; + } + }]; }]; PSCollectionViewCell *cell = selectedCell.visibleCell; if (cell) { - [self.collectionViewDelegate collectionView:self didSelectView:cell atIndex:selectedIndex]; + [self.collectionViewDelegate collectionView:self didSelectView:cell atIndexPath:selectedIndexPath]; } } From 2da8702d530e69759a912204edca20e91a3e8c5a Mon Sep 17 00:00:00 2001 From: Carson Brown Date: Thu, 20 Dec 2012 11:34:39 -0500 Subject: [PATCH 41/55] Sends a smaller available height to footers if content view is less than total frame height Otherwise, behaves as normal, sending max float --- PSCollectionView.m | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/PSCollectionView.m b/PSCollectionView.m index 6f35193..a7a9c91 100644 --- a/PSCollectionView.m +++ b/PSCollectionView.m @@ -391,7 +391,11 @@ - (CGFloat)updateFooterViewWithTotalHeight:(CGFloat)totalHeight self.footerView.width = self.width; self.footerView.top = totalHeight; - CGSize footerSize = [self.footerView sizeThatFits:CGSizeMake(self.width, CGFLOAT_MAX)]; + CGFloat availableHeight = self.height - totalHeight; + if (availableHeight <= 0.0f) { + availableHeight = CGFLOAT_MAX; + } + CGSize footerSize = [self.footerView sizeThatFits:CGSizeMake(self.width, availableHeight)]; self.footerView.height = footerSize.height; totalHeight += self.footerView.height; } From 86e301fceca9ea8111176bb0122e28cbbd60387d Mon Sep 17 00:00:00 2001 From: Carson Brown Date: Thu, 20 Dec 2012 12:01:20 -0500 Subject: [PATCH 42/55] Gives footer hint to available space below content Only if content is less than total height of scrollview's frame --- PSCollectionView.m | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/PSCollectionView.m b/PSCollectionView.m index a203230..23c2ae9 100644 --- a/PSCollectionView.m +++ b/PSCollectionView.m @@ -389,7 +389,13 @@ - (void)performLayout } //layout footer - CGSize footerSize = [self.footerView sizeThatFits:CGSizeMake(self.width, CGFLOAT_MAX)]; + NSUInteger longestColumn = [self longestColumn]; + CGFloat longestColumnHeight = [_colHeights[longestColumn] floatValue]; + CGFloat availableHeight = self.height - longestColumnHeight; + if (availableHeight <= 0.0f) { + availableHeight = CGFLOAT_MAX; + } + CGSize footerSize = [self.footerView sizeThatFits:CGSizeMake(self.width, availableHeight)]; if (self.footerView.height != footerSize.height) { self.footerView.height = footerSize.height; recalculateContentSize = YES; From b8b3a432ed3858ca65849ca0b438981ad6ee728b Mon Sep 17 00:00:00 2001 From: Adam Becevello Date: Thu, 20 Dec 2012 14:11:38 -0500 Subject: [PATCH 43/55] Fix layout issue on controllers that aren't full width --- PSCollectionView.m | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/PSCollectionView.m b/PSCollectionView.m index 23c2ae9..1f600b3 100644 --- a/PSCollectionView.m +++ b/PSCollectionView.m @@ -231,6 +231,12 @@ - (void)setFooterView:(UIView *)footerView { [self invalidateLayout]; } +- (void)setFrame:(CGRect)frame +{ + [super setFrame:frame]; + [self invalidateLayout]; +} + #pragma mark - Reset - (void)reloadData From 3f6d32bf4dc1252acdbd984b7ca5ecce76e3c543 Mon Sep 17 00:00:00 2001 From: Adam Becevello Date: Thu, 20 Dec 2012 14:47:16 -0500 Subject: [PATCH 44/55] Try to position footer properly --- PSCollectionView.m | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/PSCollectionView.m b/PSCollectionView.m index 1747547..db8b531 100644 --- a/PSCollectionView.m +++ b/PSCollectionView.m @@ -447,10 +447,14 @@ - (void)performLayout } }]; - //layout footer - NSUInteger longestColumn = [self longestColumn]; - CGFloat longestColumnHeight = [_colHeights[longestColumn] floatValue]; - CGFloat availableHeight = self.height - longestColumnHeight; + //get the height of the longest column in the last section + CGFloat availableHeight = self.height; + if (_numSections > 0) { + NSUInteger lastSection = _numSections - 1; + NSUInteger longestColumnInSection = [self longestColumnInSection:lastSection]; + CGFloat longestColumnHeight = [self yOffsetForItemInSection:lastSection column:longestColumnInSection]; + availableHeight -= longestColumnHeight; + } if (availableHeight <= 0.0f) { availableHeight = CGFLOAT_MAX; } @@ -626,17 +630,25 @@ - (NSUInteger)shortestColumnInSection:(NSUInteger)section return col; } -- (CGFloat)heightOfLongestColumnInSection:(NSUInteger)section +- (NSUInteger)longestColumnInSection:(NSUInteger)section { + NSInteger col = 0; NSMutableArray *sectionColumnHeights = _sectionColumnHeights[section]; - CGFloat maxHeight = [sectionColumnHeights[0] floatValue]; + CGFloat maxHeight = [sectionColumnHeights[col] floatValue]; for (int i = 1; i < [sectionColumnHeights count]; i++) { CGFloat colHeight = [sectionColumnHeights[i] floatValue]; if (colHeight > maxHeight) { + col = i; maxHeight = colHeight; } } - return maxHeight; + return col; +} + +- (CGFloat)heightOfLongestColumnInSection:(NSUInteger)section +{ + NSUInteger longestColumn = [self longestColumnInSection:section]; + return [_sectionColumnHeights[section][longestColumn] floatValue]; } #pragma mark - Reusing Views From 22261f4bea635469e47076d75e74edb026cbc5c1 Mon Sep 17 00:00:00 2001 From: Adam Becevello Date: Thu, 20 Dec 2012 15:39:11 -0500 Subject: [PATCH 45/55] Fix for yOffset calculation of sections --- PSCollectionView.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PSCollectionView.m b/PSCollectionView.m index db8b531..668faa9 100644 --- a/PSCollectionView.m +++ b/PSCollectionView.m @@ -598,7 +598,7 @@ - (CGFloat)yOffsetForItemInSection:(NSUInteger)section column:(NSUInteger)column } if (section > 0) { - for (NSUInteger i = 0; i < section - 1; i++) { + for (NSUInteger i = 0; i < section; i++) { height += [self heightOfLongestColumnInSection:i]; } } From 8d9f454d7a864feffd64c1a88d84ba96e527460b Mon Sep 17 00:00:00 2001 From: Adam Becevello Date: Fri, 21 Dec 2012 11:50:27 -0500 Subject: [PATCH 46/55] Section and section header support --- PSCollectionView.h | 3 + PSCollectionView.m | 185 ++++++++++++++---- PSCollectionViewItemLayoutAttributes.h | 15 ++ PSCollectionViewItemLayoutAttributes.m | 13 ++ PSCollectionViewLayoutAttributes.h | 2 - PSCollectionViewSectionViewLayoutAttributes.h | 15 ++ PSCollectionViewSectionViewLayoutAttributes.m | 13 ++ 7 files changed, 207 insertions(+), 39 deletions(-) create mode 100644 PSCollectionViewItemLayoutAttributes.h create mode 100644 PSCollectionViewItemLayoutAttributes.m create mode 100644 PSCollectionViewSectionViewLayoutAttributes.h create mode 100644 PSCollectionViewSectionViewLayoutAttributes.m diff --git a/PSCollectionView.h b/PSCollectionView.h index 94860b8..6ee7dbd 100644 --- a/PSCollectionView.h +++ b/PSCollectionView.h @@ -87,4 +87,7 @@ - (PSCollectionViewCell *)collectionView:(PSCollectionView *)collectionView viewAtIndexPath:(NSIndexPath *)indexPath; - (CGFloat)heightForViewAtIndexPath:(NSIndexPath *)indexPath; +@optional +- (UIView *)sectionHeaderForSection:(NSUInteger)section; + @end diff --git a/PSCollectionView.m b/PSCollectionView.m index 668faa9..cb3defc 100644 --- a/PSCollectionView.m +++ b/PSCollectionView.m @@ -24,6 +24,8 @@ #import "PSCollectionView.h" #import "PSCollectionViewCell.h" #import "PSCollectionViewLayoutAttributes.h" +#import "PSCollectionViewItemLayoutAttributes.h" +#import "PSCollectionViewSectionViewLayoutAttributes.h" #define kDefaultMargin 8.0 #define kAnimationDuration 0.3f @@ -114,18 +116,17 @@ @interface PSCollectionView () @implementation PSCollectionView { BOOL _batchUpdateInProgress; - - NSInteger _numSections; - UIInterfaceOrientation _orientation; - NSMutableArray *_colXOffsets; - - NSMutableArray *_sectionColumnHeights; //contains an array of arrays, first array is positioned by section number, values are the heights of each column within each section + BOOL _initialLayoutDataInitialized; NSMutableDictionary *_reusableViews; + NSMutableArray *_colXOffsets; + NSInteger _numSections; + NSMutableArray *_sectionColumnHeights; //contains an array of arrays, first array is positioned by section number, values are the heights of each column within each section NSMutableDictionary *_sectionItems; //key is section number, value is array with position is by index, value is PSCollectionViewLayoutAttribute objects + NSMutableArray *_sectionHeaders; } #pragma mark - Init/Memory @@ -148,6 +149,7 @@ - (id)initWithFrame:(CGRect)frame _orientation = [UIApplication sharedApplication].statusBarOrientation; _reusableViews = [NSMutableDictionary dictionary]; _sectionItems = [NSMutableDictionary dictionary]; + _sectionHeaders = [NSMutableArray array]; PSCollectionViewTapGestureRecognizer *recognizer = [[PSCollectionViewTapGestureRecognizer alloc] initWithTarget:self action:@selector(didSelectView:)]; [recognizer setCancelsTouchesInView:NO]; @@ -245,32 +247,68 @@ - (void)setFrame:(CGRect)frame - (void)reloadData { + _initialLayoutDataInitialized = NO; + for (NSArray *sectionItems in [_sectionItems allValues]) { - for (PSCollectionViewLayoutAttributes *attributes in sectionItems) { + for (PSCollectionViewItemLayoutAttributes *attributes in sectionItems) { [self enqueueReusableView:attributes.visibleCell]; } } + [self initializeRequiredLayoutData]; + + [self invalidateLayout]; + [self setNeedsLayout]; +} + +- (void)resetSectionHeadersFooters +{ + for (PSCollectionViewSectionViewLayoutAttributes *sectionAttributes in _sectionHeaders) { + [sectionAttributes.view removeFromSuperview]; + } + _sectionHeaders = [NSMutableArray array]; + + //retrieve the section header and footers + for (NSUInteger i = 0; i < _numSections; i++) { + PSCollectionViewSectionViewLayoutAttributes *headerAttributes = [[PSCollectionViewSectionViewLayoutAttributes alloc] init]; + if ([self.collectionViewDataSource respondsToSelector:@selector(sectionHeaderForSection:)]) { + UIView *sectionHeader = [self.collectionViewDataSource sectionHeaderForSection:i]; + headerAttributes.view = sectionHeader; + [self addSubview:sectionHeader]; + } + [_sectionHeaders addObject:headerAttributes]; + } +} + +- (void)initializeRequiredLayoutData +{ _sectionItems = [NSMutableDictionary dictionary]; _numSections = [self.collectionViewDataSource numberOfSectionsInCollectionView:self]; + //ensure there are entries for earlier sections + for (NSUInteger i = [_sectionColumnHeights count]; i < _numSections; i++) { + [_sectionColumnHeights addObject:[NSMutableArray array]]; + [self resetColumnHeightsInSection:i]; + } + for (NSUInteger i = 0; i < _numSections; i++) { //recreate items for the number of views that will appear in the grid NSInteger numCells = [self.collectionViewDataSource collectionView:self numberOfViewsInSection:i]; NSMutableArray *items = [NSMutableArray arrayWithCapacity:numCells]; for (NSUInteger i = 0; i < numCells; i++) { - [items addObject:[[PSCollectionViewLayoutAttributes alloc] init]]; + [items addObject:[[PSCollectionViewItemLayoutAttributes alloc] init]]; } [_sectionItems setObject:items forKey:@(i)]; } - [self invalidateLayout]; - [self setNeedsLayout]; + [self resetSectionHeadersFooters]; + + _initialLayoutDataInitialized = YES; } #pragma mark - View -- (void)invalidateItemLayoutAttributes:(PSCollectionViewLayoutAttributes *)attributes +- (void)invalidateItemLayoutAttributes:(PSCollectionViewItemLayoutAttributes *)attributes { attributes.valid = NO; attributes.frame = CGRectZero; @@ -278,8 +316,11 @@ - (void)invalidateItemLayoutAttributes:(PSCollectionViewLayoutAttributes *)attri - (void)invalidateLayout { + for (PSCollectionViewSectionViewLayoutAttributes *sectionHeader in _sectionHeaders) { + sectionHeader.valid = NO; + } for (NSArray *sectionItems in [_sectionItems allValues]) { - [sectionItems enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(PSCollectionViewLayoutAttributes *attributes, NSUInteger idx, BOOL *stop) { + [sectionItems enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(PSCollectionViewItemLayoutAttributes *attributes, NSUInteger idx, BOOL *stop) { [self invalidateItemLayoutAttributes:attributes]; }]; } @@ -295,6 +336,8 @@ - (void)invalidateLayout } else { self.colWidth = floorf((self.width - self.margin * (self.numCols + 1)) / self.numCols); } + + [self resetSectionHeadersFooters]; } - (void)resetColumnHeights @@ -302,7 +345,7 @@ - (void)resetColumnHeights //sections are assumed to be the same height, this will change as items are added to each sections _sectionColumnHeights = [NSMutableArray array]; for (NSUInteger i = 0; i < _numSections; i++) { - [_sectionColumnHeights addObject:@0.0f]; //ensure the array has a position for this section, this simplifies the resetColumnHeightsInSection method + [_sectionColumnHeights addObject:[NSMutableArray array]]; //ensure the array has a position for this section, this simplifies the resetColumnHeightsInSection method [self resetColumnHeightsInSection:i]; } } @@ -333,6 +376,10 @@ - (void)layoutSubviews - (void)performLayout { + if (_initialLayoutDataInitialized == NO) { + [self initializeRequiredLayoutData]; + } + __block BOOL recalculateContentSize = NO; //layout header @@ -373,9 +420,24 @@ - (void)performLayout //calculate the positions of all cells, but skip the cells that had no change //a cell has a change if its layout is marked as invalid - [_sectionItems enumerateKeysAndObjectsUsingBlock:^(NSNumber *sectionNumber, NSArray *sectionItems, BOOL *stop) { - NSUInteger section = [sectionNumber integerValue]; - [sectionItems enumerateObjectsUsingBlock:^(PSCollectionViewLayoutAttributes *itemAttributes, NSUInteger idx, BOOL *stop) { + for (NSUInteger section = 0; section < _numSections; section++) { + NSNumber *sectionNumber = @(section); + + //layout the section header + PSCollectionViewSectionViewLayoutAttributes *sectionHeaderAttributes = [self sectionHeaderAttributesForSection:section]; + if (sectionHeaderAttributes.view && sectionHeaderAttributes.valid == NO) { + UIView *sectionHeader = sectionHeaderAttributes.view; + CGSize headerSize = [sectionHeader sizeThatFits:CGSizeMake(self.width, CGFLOAT_MAX)]; + CGFloat yOffset = [self yOffsetForBeginningOfSection:section]; + sectionHeader.frame = CGRectMake(CGRectGetMinX(self.bounds), yOffset, self.width, headerSize.height); + sectionHeaderAttributes.valid = YES; + + recalculateContentSize = YES; + } + + //layout the section items + NSMutableArray *sectionItems = _sectionItems[sectionNumber]; + [sectionItems enumerateObjectsUsingBlock:^(PSCollectionViewItemLayoutAttributes *itemAttributes, NSUInteger idx, BOOL *stop) { if (itemAttributes.valid == NO) { NSIndexPath *indexPath = [NSIndexPath indexPathForItem:idx inSection:section]; @@ -409,14 +471,14 @@ - (void)performLayout } } }]; - }]; + }; //Lays out items that are now visible and hides cells that are no longer visible CGRect visibleRect = CGRectMake(self.contentOffset.x, self.contentOffset.y, self.width, self.height); [_sectionItems enumerateKeysAndObjectsUsingBlock:^(NSNumber *sectionNumber, NSArray *sectionItems, BOOL *stop) { for (NSUInteger i = 0; i < [sectionItems count]; i++) { - PSCollectionViewLayoutAttributes *itemAttributes = sectionItems[i]; + PSCollectionViewItemLayoutAttributes *itemAttributes = sectionItems[i]; BOOL visibleCell = CGRectIntersectsRect(visibleRect, itemAttributes.frame); if (visibleCell == NO && itemAttributes.visibleCell) { //Cell isn't visible, hide it @@ -472,7 +534,7 @@ - (void)performLayout - (void)insertItemAtIndexPath:(NSIndexPath *)indexPath { - PSCollectionViewLayoutAttributes *attributes = [[PSCollectionViewLayoutAttributes alloc] init]; + PSCollectionViewItemLayoutAttributes *attributes = [[PSCollectionViewItemLayoutAttributes alloc] init]; NSNumber *section = @(indexPath.section); NSMutableArray *sectionItems = [_sectionItems objectForKey:section]; @@ -495,7 +557,7 @@ - (void)removeItemAtIndexPath:(NSIndexPath *)indexPath NSNumber *section = @(indexPath.section); NSMutableArray *sectionItems = [_sectionItems objectForKey:section]; if (sectionItems) { - PSCollectionViewLayoutAttributes *attributes = sectionItems[indexPath.item]; + PSCollectionViewItemLayoutAttributes *attributes = sectionItems[indexPath.item]; [self enqueueReusableView:attributes.visibleCell]; [sectionItems removeObjectAtIndex:indexPath.item]; @@ -524,23 +586,29 @@ - (void)performBatchUpdates:(void (^)(void))updates completion:(void (^)(void))c - (void)invalidateLayoutOfItemsAfterIndexPath:(NSIndexPath *)indexPath { + //invalidate all section headers for all subsequent sections + for (NSUInteger section = indexPath.section + 1; section < [_sectionHeaders count]; section++) { + PSCollectionViewSectionViewLayoutAttributes *sectionHeader = _sectionHeaders[section]; + sectionHeader.valid = NO; + } + [_sectionItems enumerateKeysAndObjectsUsingBlock:^(NSNumber *sectionNumber, NSMutableArray *sectionItems, BOOL *stop) { NSInteger section = [sectionNumber integerValue]; - if (indexPath.section > section) { + if (section > indexPath.section) { //invalidate all items in this section - for (PSCollectionViewLayoutAttributes *attributes in sectionItems) { + //the column heights don't need to be invalidated since the section as a whole will be moved + for (PSCollectionViewItemLayoutAttributes *attributes in sectionItems) { [self invalidateItemLayoutAttributes:attributes]; } - [self resetColumnHeightsInSection:section]; } else if (indexPath.section == section) { //invalidate only items after this item for (int i=indexPath.item; i < [sectionItems count]; i++) { - PSCollectionViewLayoutAttributes *attributes = sectionItems[i]; + PSCollectionViewItemLayoutAttributes *attributes = sectionItems[i]; [self invalidateItemLayoutAttributes:attributes]; } [self resetColumnHeightsInSection:section]; } - }]; + }]; //get the max Y values from the previous elements in each column (only need to get numCols number of elements) //this does not update the content size since that will be done in performLayout once all the batched changes are applied @@ -548,14 +616,18 @@ - (void)invalidateLayoutOfItemsAfterIndexPath:(NSIndexPath *)indexPath NSMutableIndexSet *completedColumns = [NSMutableIndexSet indexSet]; NSInteger i = indexPath.item - 1; NSMutableArray *sectionItems = [_sectionItems objectForKey:@(indexPath.section)]; + CGFloat yOffsetOfSection = [self yOffsetForBeginningOfSection:indexPath.section]; while (i >= 0) { - PSCollectionViewLayoutAttributes *attributes = sectionItems[i]; + PSCollectionViewItemLayoutAttributes *attributes = sectionItems[i]; if (attributes.valid && [completedColumns containsIndex:attributes.currentColumn] == NO) { + PSCollectionViewSectionViewLayoutAttributes *sectionHeader = [self sectionHeaderAttributesForSection:indexPath.section]; + NSMutableArray *colHeights = _sectionColumnHeights[indexPath.section]; - colHeights[attributes.currentColumn] = @(CGRectGetMaxY(attributes.frame) - self.headerView.height + self.margin); + CGFloat height = CGRectGetMaxY(attributes.frame) - yOffsetOfSection; + colHeights[attributes.currentColumn] = @(height - sectionHeader.view.height + self.margin); [completedColumns addIndex:attributes.currentColumn]; } - //stop checking if all columns have been updated + //if all columns have been updated, stop checking if ([completedColumns count] == self.numCols) { break; } @@ -573,8 +645,9 @@ - (void)updateContentSizeForColumnHeightChange totalHeight += self.headerView.height; } + //heights of all sections for (NSUInteger i = 0; i < _numSections; i++) { - totalHeight += [self heightOfLongestColumnInSection:i]; + totalHeight += [self heightOfSection:i]; } if (self.footerView) { @@ -588,21 +661,59 @@ - (void)updateContentSizeForColumnHeightChange #pragma mark - Helpers -- (CGFloat)yOffsetForItemInSection:(NSUInteger)section column:(NSUInteger)column +- (PSCollectionViewSectionViewLayoutAttributes *)sectionHeaderAttributesForSection:(NSUInteger)section +{ + if (section < [_sectionHeaders count]) { + return _sectionHeaders[section]; + } + return nil; +} + +- (CGFloat)heightOfSection:(NSUInteger)section +{ + CGFloat height = 0.0f; + + PSCollectionViewSectionViewLayoutAttributes *header = [self sectionHeaderAttributesForSection:section]; + if (header.view) { + height += header.view.height; + } + + height += [self heightOfLongestColumnInSection:section]; + + return height; +} + +- (CGFloat)yOffsetForBeginningOfSection:(NSUInteger)section { //the yoffset is the height of all previous sections, plus the height of the column in the current section CGFloat height = 0.0f; + //collection view header if (self.headerView) { height += self.headerView.height; } + //add the heights of all previous sections if (section > 0) { for (NSUInteger i = 0; i < section; i++) { - height += [self heightOfLongestColumnInSection:i]; + height += [self heightOfSection:i]; } } + return height; +} + +- (CGFloat)yOffsetForItemInSection:(NSUInteger)section column:(NSUInteger)column +{ + //the yoffset is the height of all previous sections, plus the height of the column in the current section + CGFloat height = [self yOffsetForBeginningOfSection:section]; + + //section header height + PSCollectionViewSectionViewLayoutAttributes *sectionHeaderAttributes = [self sectionHeaderAttributesForSection:section]; + if (sectionHeaderAttributes.view) { + height += sectionHeaderAttributes.view.height; + } + NSArray *columnHeights = _sectionColumnHeights[section]; height += [columnHeights[column] floatValue]; return height; @@ -633,10 +744,10 @@ - (NSUInteger)shortestColumnInSection:(NSUInteger)section - (NSUInteger)longestColumnInSection:(NSUInteger)section { NSInteger col = 0; - NSMutableArray *sectionColumnHeights = _sectionColumnHeights[section]; - CGFloat maxHeight = [sectionColumnHeights[col] floatValue]; - for (int i = 1; i < [sectionColumnHeights count]; i++) { - CGFloat colHeight = [sectionColumnHeights[i] floatValue]; + NSMutableArray *columnHeights = _sectionColumnHeights[section]; + CGFloat maxHeight = [columnHeights[col] floatValue]; + for (int i = 1; i < [columnHeights count]; i++) { + CGFloat colHeight = [columnHeights[i] floatValue]; if (colHeight > maxHeight) { col = i; maxHeight = colHeight; @@ -695,10 +806,10 @@ - (void)didSelectView:(UITapGestureRecognizer *)gestureRecognizer { CGPoint tapPoint = [gestureRecognizer locationInView:self]; - __block PSCollectionViewLayoutAttributes *selectedCell = nil; + __block PSCollectionViewItemLayoutAttributes *selectedCell = nil; __block NSIndexPath *selectedIndexPath = nil; [_sectionItems enumerateKeysAndObjectsUsingBlock:^(NSNumber *sectionNumber, NSArray *sectionItems, BOOL *stop) { - [sectionItems enumerateObjectsUsingBlock:^(PSCollectionViewLayoutAttributes *candidate, NSUInteger idx, BOOL *stop) { + [sectionItems enumerateObjectsUsingBlock:^(PSCollectionViewItemLayoutAttributes *candidate, NSUInteger idx, BOOL *stop) { if (candidate.valid && CGRectContainsPoint(candidate.frame, tapPoint)) { selectedCell = candidate; selectedIndexPath = [NSIndexPath indexPathForItem:idx inSection:[sectionNumber integerValue]]; diff --git a/PSCollectionViewItemLayoutAttributes.h b/PSCollectionViewItemLayoutAttributes.h new file mode 100644 index 0000000..10854b7 --- /dev/null +++ b/PSCollectionViewItemLayoutAttributes.h @@ -0,0 +1,15 @@ +// +// PSCollectionViewItemLayoutAttributes.h +// ShopByShopify +// +// Created by Adam Becevello on 2012-12-20. +// Copyright (c) 2012 Shopify Inc. All rights reserved. +// + +#import "PSCollectionViewLayoutAttributes.h" + +@interface PSCollectionViewItemLayoutAttributes : PSCollectionViewLayoutAttributes + +@property (nonatomic, strong) PSCollectionViewCell *visibleCell; + +@end diff --git a/PSCollectionViewItemLayoutAttributes.m b/PSCollectionViewItemLayoutAttributes.m new file mode 100644 index 0000000..d8fe27e --- /dev/null +++ b/PSCollectionViewItemLayoutAttributes.m @@ -0,0 +1,13 @@ +// +// PSCollectionViewItemLayoutAttributes.m +// ShopByShopify +// +// Created by Adam Becevello on 2012-12-20. +// Copyright (c) 2012 Shopify Inc. All rights reserved. +// + +#import "PSCollectionViewItemLayoutAttributes.h" + +@implementation PSCollectionViewItemLayoutAttributes + +@end diff --git a/PSCollectionViewLayoutAttributes.h b/PSCollectionViewLayoutAttributes.h index 0db4c96..c26164d 100644 --- a/PSCollectionViewLayoutAttributes.h +++ b/PSCollectionViewLayoutAttributes.h @@ -16,6 +16,4 @@ @property (nonatomic, assign) BOOL valid; @property (nonatomic, assign) BOOL previouslyVisible; -@property (nonatomic, strong) PSCollectionViewCell *visibleCell; - @end diff --git a/PSCollectionViewSectionViewLayoutAttributes.h b/PSCollectionViewSectionViewLayoutAttributes.h new file mode 100644 index 0000000..21007aa --- /dev/null +++ b/PSCollectionViewSectionViewLayoutAttributes.h @@ -0,0 +1,15 @@ +// +// PSCollectionViewSectionViewLayoutAttributes.h +// ShopByShopify +// +// Created by Adam Becevello on 2012-12-20. +// Copyright (c) 2012 Shopify Inc. All rights reserved. +// + +#import "PSCollectionViewLayoutAttributes.h" + +@interface PSCollectionViewSectionViewLayoutAttributes : PSCollectionViewLayoutAttributes + +@property (nonatomic, readwrite, strong) UIView *view; + +@end diff --git a/PSCollectionViewSectionViewLayoutAttributes.m b/PSCollectionViewSectionViewLayoutAttributes.m new file mode 100644 index 0000000..ac5df23 --- /dev/null +++ b/PSCollectionViewSectionViewLayoutAttributes.m @@ -0,0 +1,13 @@ +// +// PSCollectionViewSectionViewLayoutAttributes.m +// ShopByShopify +// +// Created by Adam Becevello on 2012-12-20. +// Copyright (c) 2012 Shopify Inc. All rights reserved. +// + +#import "PSCollectionViewSectionViewLayoutAttributes.h" + +@implementation PSCollectionViewSectionViewLayoutAttributes + +@end From aa778b459844416130ea6e1b3d18be67012cb3eb Mon Sep 17 00:00:00 2001 From: Adam Becevello Date: Thu, 3 Jan 2013 15:28:18 -0500 Subject: [PATCH 47/55] Properly animation section header frame changes @carsonb this should fix the animation issue you were seeing --- PSCollectionView.m | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/PSCollectionView.m b/PSCollectionView.m index cb3defc..96ac1e0 100644 --- a/PSCollectionView.m +++ b/PSCollectionView.m @@ -429,9 +429,20 @@ - (void)performLayout UIView *sectionHeader = sectionHeaderAttributes.view; CGSize headerSize = [sectionHeader sizeThatFits:CGSizeMake(self.width, CGFLOAT_MAX)]; CGFloat yOffset = [self yOffsetForBeginningOfSection:section]; - sectionHeader.frame = CGRectMake(CGRectGetMinX(self.bounds), yOffset, self.width, headerSize.height); + CGRect frame = CGRectMake(CGRectGetMinX(self.bounds), yOffset, self.width, headerSize.height); sectionHeaderAttributes.valid = YES; + //animations shouldn't happen if the section header hasn't had a frame yet + if (self.animateLayoutChanges && CGRectEqualToRect(sectionHeader.frame, CGRectZero) == NO) { + [UIView animateWithDuration:kAnimationDuration animations:^{ + sectionHeader.frame = frame; + }]; + } else { + [UIView setAnimationsEnabled:NO]; + sectionHeader.frame = frame; + [UIView setAnimationsEnabled:YES]; + } + recalculateContentSize = YES; } From 8bd9b2094a08aa8c59515c70772d4565681d62d3 Mon Sep 17 00:00:00 2001 From: Carson Brown Date: Mon, 7 Jan 2013 09:48:10 -0500 Subject: [PATCH 48/55] Adjusts data source methods to contain handle to collection view --- PSCollectionView.h | 4 ++-- PSCollectionView.m | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PSCollectionView.h b/PSCollectionView.h index 6ee7dbd..30275ea 100644 --- a/PSCollectionView.h +++ b/PSCollectionView.h @@ -85,9 +85,9 @@ - (NSUInteger)numberOfSectionsInCollectionView:(PSCollectionView *)collectionView; - (NSUInteger)collectionView:(PSCollectionView *)collectionView numberOfViewsInSection:(NSUInteger)section; - (PSCollectionViewCell *)collectionView:(PSCollectionView *)collectionView viewAtIndexPath:(NSIndexPath *)indexPath; -- (CGFloat)heightForViewAtIndexPath:(NSIndexPath *)indexPath; +- (CGFloat)collectionView:(PSCollectionView *)collectionView heightForViewAtIndexPath:(NSIndexPath *)indexPath; @optional -- (UIView *)sectionHeaderForSection:(NSUInteger)section; +- (UIView *)collectionView:(PSCollectionView *)collectionView sectionHeaderForSection:(NSUInteger)section; @end diff --git a/PSCollectionView.m b/PSCollectionView.m index 96ac1e0..4b24cef 100644 --- a/PSCollectionView.m +++ b/PSCollectionView.m @@ -271,8 +271,8 @@ - (void)resetSectionHeadersFooters //retrieve the section header and footers for (NSUInteger i = 0; i < _numSections; i++) { PSCollectionViewSectionViewLayoutAttributes *headerAttributes = [[PSCollectionViewSectionViewLayoutAttributes alloc] init]; - if ([self.collectionViewDataSource respondsToSelector:@selector(sectionHeaderForSection:)]) { - UIView *sectionHeader = [self.collectionViewDataSource sectionHeaderForSection:i]; + if ([self.collectionViewDataSource respondsToSelector:@selector(collectionView:sectionHeaderForSection:)]) { + UIView *sectionHeader = [self.collectionViewDataSource collectionView:self sectionHeaderForSection:i]; headerAttributes.view = sectionHeader; [self addSubview:sectionHeader]; } @@ -455,7 +455,7 @@ - (void)performLayout //ensure we have the height for this item CGFloat height = itemAttributes.frame.size.height; if (height == 0.0f) { - height = [self.collectionViewDataSource heightForViewAtIndexPath:indexPath]; + height = [self.collectionViewDataSource collectionView:self heightForViewAtIndexPath:indexPath]; } //find the shortest column From edab5fa495631030535b4eb7e69cc36a5b779c58 Mon Sep 17 00:00:00 2001 From: Adam Becevello Date: Fri, 11 Jan 2013 09:13:50 -0500 Subject: [PATCH 49/55] Properly reset item attributes when invalidating layout --- PSCollectionView.m | 1 + 1 file changed, 1 insertion(+) diff --git a/PSCollectionView.m b/PSCollectionView.m index 4b24cef..5490c39 100644 --- a/PSCollectionView.m +++ b/PSCollectionView.m @@ -312,6 +312,7 @@ - (void)invalidateItemLayoutAttributes:(PSCollectionViewItemLayoutAttributes *)a { attributes.valid = NO; attributes.frame = CGRectZero; + attributes.previouslyVisible = NO; } - (void)invalidateLayout From d3cf894c984f6a66cff4792c9aa50f4dd9c7f95b Mon Sep 17 00:00:00 2001 From: Adam Becevello Date: Fri, 18 Jan 2013 14:32:23 -0500 Subject: [PATCH 50/55] Don't animate section appearances Desk reviewed by @carsonb --- PSCollectionView.m | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/PSCollectionView.m b/PSCollectionView.m index 5490c39..2fb41ef 100644 --- a/PSCollectionView.m +++ b/PSCollectionView.m @@ -319,6 +319,7 @@ - (void)invalidateLayout { for (PSCollectionViewSectionViewLayoutAttributes *sectionHeader in _sectionHeaders) { sectionHeader.valid = NO; + sectionHeader.previouslyVisible = NO; } for (NSArray *sectionItems in [_sectionItems allValues]) { [sectionItems enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(PSCollectionViewItemLayoutAttributes *attributes, NSUInteger idx, BOOL *stop) { @@ -434,7 +435,7 @@ - (void)performLayout sectionHeaderAttributes.valid = YES; //animations shouldn't happen if the section header hasn't had a frame yet - if (self.animateLayoutChanges && CGRectEqualToRect(sectionHeader.frame, CGRectZero) == NO) { + if (self.animateLayoutChanges && sectionHeaderAttributes.previouslyVisible && CGRectEqualToRect(sectionHeader.frame, CGRectZero) == NO) { [UIView animateWithDuration:kAnimationDuration animations:^{ sectionHeader.frame = frame; }]; @@ -443,6 +444,7 @@ - (void)performLayout sectionHeader.frame = frame; [UIView setAnimationsEnabled:YES]; } + sectionHeaderAttributes.previouslyVisible = YES; recalculateContentSize = YES; } From 862d17c5c471259f1fe6addb31c8975bd593ea98 Mon Sep 17 00:00:00 2001 From: Adam Becevello Date: Tue, 22 Jan 2013 15:21:58 -0500 Subject: [PATCH 51/55] Fix issue with multiple sections with layout --- PSCollectionView.m | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/PSCollectionView.m b/PSCollectionView.m index 2fb41ef..01dc7e5 100644 --- a/PSCollectionView.m +++ b/PSCollectionView.m @@ -315,11 +315,16 @@ - (void)invalidateItemLayoutAttributes:(PSCollectionViewItemLayoutAttributes *)a attributes.previouslyVisible = NO; } +- (void)invalidateSectionLayoutAttributes:(PSCollectionViewSectionViewLayoutAttributes *)attributes +{ + attributes.valid = NO; + attributes.previouslyVisible = NO; +} + - (void)invalidateLayout { for (PSCollectionViewSectionViewLayoutAttributes *sectionHeader in _sectionHeaders) { - sectionHeader.valid = NO; - sectionHeader.previouslyVisible = NO; + [self invalidateSectionLayoutAttributes:sectionHeader]; } for (NSArray *sectionItems in [_sectionItems allValues]) { [sectionItems enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(PSCollectionViewItemLayoutAttributes *attributes, NSUInteger idx, BOOL *stop) { @@ -603,7 +608,7 @@ - (void)invalidateLayoutOfItemsAfterIndexPath:(NSIndexPath *)indexPath //invalidate all section headers for all subsequent sections for (NSUInteger section = indexPath.section + 1; section < [_sectionHeaders count]; section++) { PSCollectionViewSectionViewLayoutAttributes *sectionHeader = _sectionHeaders[section]; - sectionHeader.valid = NO; + [self invalidateSectionLayoutAttributes:sectionHeader]; } [_sectionItems enumerateKeysAndObjectsUsingBlock:^(NSNumber *sectionNumber, NSMutableArray *sectionItems, BOOL *stop) { @@ -620,8 +625,8 @@ - (void)invalidateLayoutOfItemsAfterIndexPath:(NSIndexPath *)indexPath PSCollectionViewItemLayoutAttributes *attributes = sectionItems[i]; [self invalidateItemLayoutAttributes:attributes]; } - [self resetColumnHeightsInSection:section]; } + [self resetColumnHeightsInSection:section]; }]; //get the max Y values from the previous elements in each column (only need to get numCols number of elements) From 9da7a45555c0e813e90000b802263eecc5beb4d9 Mon Sep 17 00:00:00 2001 From: Adam Becevello Date: Tue, 22 Jan 2013 19:41:29 -0500 Subject: [PATCH 52/55] Don't reset column heights for sections that aren't being updated --- PSCollectionView.m | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/PSCollectionView.m b/PSCollectionView.m index 01dc7e5..470cf55 100644 --- a/PSCollectionView.m +++ b/PSCollectionView.m @@ -619,14 +619,15 @@ - (void)invalidateLayoutOfItemsAfterIndexPath:(NSIndexPath *)indexPath for (PSCollectionViewItemLayoutAttributes *attributes in sectionItems) { [self invalidateItemLayoutAttributes:attributes]; } + [self resetColumnHeightsInSection:section]; } else if (indexPath.section == section) { //invalidate only items after this item for (int i=indexPath.item; i < [sectionItems count]; i++) { PSCollectionViewItemLayoutAttributes *attributes = sectionItems[i]; [self invalidateItemLayoutAttributes:attributes]; } + [self resetColumnHeightsInSection:section]; } - [self resetColumnHeightsInSection:section]; }]; //get the max Y values from the previous elements in each column (only need to get numCols number of elements) From 4c429a9f9cfa1e47d651144b46b52d0acb31d0e0 Mon Sep 17 00:00:00 2001 From: Adam Becevello Date: Sat, 2 Mar 2013 10:18:10 -0500 Subject: [PATCH 53/55] Update README.md --- README.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9f8d8f5..cfdeaa2 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,15 @@ It's a Pinterest style scroll view designed to be used similar to a UITableView. It supports Portrait and Landscape orientations. -I built this as a hack to show my friends. Any suggestions or improvements are very welcome! +This version is an almost complete rewrite of the layout code to support additional features such as: +1) Multiple sections +2) Section headers and footers +3) Improved performance and reduced memory usage, especially during fast scrolling operations. -Coming soon... A fully functional demo app. +Due to the inclusion of multiple section support, this fork is not API compatible with the original PSCollectionView found at https://github.com/ptshih/PSCollectionView +Thanks to Peter Shih (https://github.com/ptshih) for writing the original PSCollectionView which forms the basis of this version. + +The rest of this README is the same as the README in the original PSCollectionView repository. What is PSCollectionViewCell? --- @@ -126,4 +132,4 @@ THE SOFTWARE. Questions? --- -Feel free to send me an email if you have any questions implementing PSCollectionView! \ No newline at end of file +Feel free to send me an email if you have any questions implementing PSCollectionView! From 87bda5b4f34f69182c2e6a3ea46088d9fba739a2 Mon Sep 17 00:00:00 2001 From: Adam Becevello Date: Sat, 2 Mar 2013 10:19:34 -0500 Subject: [PATCH 54/55] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index cfdeaa2..d271b65 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,9 @@ It's a Pinterest style scroll view designed to be used similar to a UITableView. It supports Portrait and Landscape orientations. This version is an almost complete rewrite of the layout code to support additional features such as: -1) Multiple sections -2) Section headers and footers -3) Improved performance and reduced memory usage, especially during fast scrolling operations. + 1. Multiple sections + 2. Section headers and footers + 3. Improved performance and reduced memory usage, especially during fast scrolling operations. Due to the inclusion of multiple section support, this fork is not API compatible with the original PSCollectionView found at https://github.com/ptshih/PSCollectionView Thanks to Peter Shih (https://github.com/ptshih) for writing the original PSCollectionView which forms the basis of this version. From 24254b2f3d6338a65b81d85f6fd7d70f664ad893 Mon Sep 17 00:00:00 2001 From: Adam Becevello Date: Sat, 2 Mar 2013 10:20:03 -0500 Subject: [PATCH 55/55] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d271b65..af0ecfa 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ This version is an almost complete rewrite of the layout code to support additio 2. Section headers and footers 3. Improved performance and reduced memory usage, especially during fast scrolling operations. -Due to the inclusion of multiple section support, this fork is not API compatible with the original PSCollectionView found at https://github.com/ptshih/PSCollectionView +Due to the inclusion of multiple section support, this fork is not API compatible with the original PSCollectionView found at https://github.com/ptshih/PSCollectionView. Thanks to Peter Shih (https://github.com/ptshih) for writing the original PSCollectionView which forms the basis of this version. The rest of this README is the same as the README in the original PSCollectionView repository.