├── .gitignore ├── LICENSE ├── PSCollectionView.h ├── PSCollectionView.m ├── PSCollectionViewCell.h ├── PSCollectionViewCell.m └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Macj 2 | .DS_Store 3 | *.swp 4 | *~.nib 5 | 6 | # Xcode 7 | build/* 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | *.xcworkspace 17 | !default.xcworkspace 18 | xcuserdata 19 | profile 20 | *.moved-aside 21 | Info.h 22 | 23 | # PSKit 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Peter Shih (http://petershih.com) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /PSCollectionView.h: -------------------------------------------------------------------------------- 1 | // 2 | // PSCollectionView.h 3 | // 4 | // Copyright (c) 2012 Peter Stone (https://www.impeterstone.com) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | #import 25 | #import "PSCollectionViewCell.h" 26 | 27 | #define kPSCollectionViewDidRelayoutNotification @"kPSCollectionViewDidRelayoutNotification" 28 | 29 | @class PSCollectionViewCell; 30 | 31 | @protocol PSCollectionViewDelegate, PSCollectionViewDataSource; 32 | 33 | @interface PSCollectionView : UIScrollView 34 | 35 | #pragma mark - Public Properties 36 | 37 | @property (nonatomic, strong) UIView *headerView; 38 | @property (nonatomic, strong) UIView *footerView; 39 | 40 | @property (nonatomic, assign, readonly) CGFloat colWidth; 41 | @property (nonatomic, assign, readonly) NSInteger numCols; 42 | @property (nonatomic, assign) NSInteger numColsLandscape; 43 | @property (nonatomic, assign) NSInteger numColsPortrait; 44 | @property (nonatomic, unsafe_unretained) id collectionViewDelegate; 45 | @property (nonatomic, unsafe_unretained) id collectionViewDataSource; 46 | 47 | #pragma mark - Public Methods 48 | 49 | /** 50 | Reloads the collection view 51 | This is similar to UITableView reloadData) 52 | */ 53 | - (void)reloadData; 54 | 55 | /** 56 | Dequeues a reusable view that was previously initialized 57 | This is similar to UITableView dequeueReusableCellWithIdentifier 58 | */ 59 | - (PSCollectionViewCell *)dequeueReusableViewForClass:(Class)viewClass; 60 | 61 | @end 62 | 63 | #pragma mark - Delegate 64 | 65 | @protocol PSCollectionViewDelegate 66 | 67 | @optional 68 | - (void)collectionView:(PSCollectionView *)collectionView didSelectCell:(PSCollectionViewCell *)cell atIndex:(NSInteger)index; 69 | - (Class)collectionView:(PSCollectionView *)collectionView cellClassForRowAtIndex:(NSInteger)index; 70 | 71 | @end 72 | 73 | #pragma mark - DataSource 74 | 75 | @protocol PSCollectionViewDataSource 76 | 77 | @required 78 | - (NSInteger)numberOfRowsInCollectionView:(PSCollectionView *)collectionView; 79 | - (PSCollectionViewCell *)collectionView:(PSCollectionView *)collectionView cellForRowAtIndex:(NSInteger)index; 80 | - (CGFloat)collectionView:(PSCollectionView *)collectionView heightForRowAtIndex:(NSInteger)index; 81 | 82 | @end 83 | -------------------------------------------------------------------------------- /PSCollectionView.m: -------------------------------------------------------------------------------- 1 | // 2 | // PSCollectionView.m 3 | // 4 | // Copyright (c) 2012 Peter Stone (https://www.impeterstone.com) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | #import "PSCollectionView.h" 25 | 26 | #define kMargin 8.0 27 | 28 | static inline NSString * PSCollectionKeyForIndex(NSInteger index) { 29 | return [NSString stringWithFormat:@"%d", index]; 30 | } 31 | 32 | static inline NSInteger PSCollectionIndexForKey(NSString *key) { 33 | return [key integerValue]; 34 | } 35 | 36 | #pragma mark - UIView Category 37 | 38 | @interface UIView (PSCollectionView) 39 | 40 | @property(nonatomic, assign) CGFloat left; 41 | @property(nonatomic, assign) CGFloat top; 42 | @property(nonatomic, assign, readonly) CGFloat right; 43 | @property(nonatomic, assign, readonly) CGFloat bottom; 44 | @property(nonatomic, assign) CGFloat width; 45 | @property(nonatomic, assign) CGFloat height; 46 | 47 | @end 48 | 49 | @implementation UIView (PSCollectionView) 50 | 51 | - (CGFloat)left { 52 | return self.frame.origin.x; 53 | } 54 | 55 | - (void)setLeft:(CGFloat)x { 56 | CGRect frame = self.frame; 57 | frame.origin.x = x; 58 | self.frame = frame; 59 | } 60 | 61 | - (CGFloat)top { 62 | return self.frame.origin.y; 63 | } 64 | 65 | - (void)setTop:(CGFloat)y { 66 | CGRect frame = self.frame; 67 | frame.origin.y = y; 68 | self.frame = frame; 69 | } 70 | 71 | - (CGFloat)right { 72 | return self.frame.origin.x + self.frame.size.width; 73 | } 74 | 75 | - (CGFloat)bottom { 76 | return self.frame.origin.y + self.frame.size.height; 77 | } 78 | 79 | - (CGFloat)width { 80 | return self.frame.size.width; 81 | } 82 | 83 | - (void)setWidth:(CGFloat)width { 84 | CGRect frame = self.frame; 85 | frame.size.width = width; 86 | self.frame = frame; 87 | } 88 | 89 | - (CGFloat)height { 90 | return self.frame.size.height; 91 | } 92 | 93 | - (void)setHeight:(CGFloat)height { 94 | CGRect frame = self.frame; 95 | frame.size.height = height; 96 | self.frame = frame; 97 | } 98 | 99 | @end 100 | 101 | #pragma mark - Gesture Recognizer 102 | 103 | // This is just so we know that we sent this tap gesture recognizer in the delegate 104 | @interface PSCollectionViewTapGestureRecognizer : UITapGestureRecognizer 105 | @end 106 | 107 | @implementation PSCollectionViewTapGestureRecognizer 108 | @end 109 | 110 | 111 | @interface PSCollectionView () 112 | 113 | @property (nonatomic, assign, readwrite) CGFloat lastOffset; 114 | @property (nonatomic, assign, readwrite) CGFloat offsetThreshold; 115 | @property (nonatomic, assign, readwrite) CGFloat lastWidth; 116 | @property (nonatomic, assign, readwrite) CGFloat colWidth; 117 | @property (nonatomic, assign, readwrite) NSInteger numCols; 118 | @property (nonatomic, assign) UIInterfaceOrientation orientation; 119 | 120 | @property (nonatomic, strong) NSMutableDictionary *reuseableViews; 121 | @property (nonatomic, strong) NSMutableDictionary *visibleViews; 122 | @property (nonatomic, strong) NSMutableArray *viewKeysToRemove; 123 | @property (nonatomic, strong) NSMutableDictionary *indexToRectMap; 124 | 125 | 126 | /** 127 | Forces a relayout of the collection grid 128 | */ 129 | - (void)relayoutViews; 130 | 131 | /** 132 | Stores a view for later reuse 133 | TODO: add an identifier like UITableView 134 | */ 135 | - (void)enqueueReusableView:(PSCollectionViewCell *)view; 136 | 137 | /** 138 | Magic! 139 | */ 140 | - (void)removeAndAddCellsIfNecessary; 141 | 142 | @end 143 | 144 | @implementation PSCollectionView 145 | 146 | #pragma mark - Init/Memory 147 | 148 | - (id)initWithFrame:(CGRect)frame { 149 | self = [super initWithFrame:frame]; 150 | if (self) { 151 | self.alwaysBounceVertical = YES; 152 | 153 | self.lastOffset = 0.0; 154 | self.offsetThreshold = floorf(self.height / 4.0); 155 | 156 | self.colWidth = 0.0; 157 | self.numCols = 0; 158 | self.numColsPortrait = 0; 159 | self.numColsLandscape = 0; 160 | self.orientation = [UIApplication sharedApplication].statusBarOrientation; 161 | 162 | self.reuseableViews = [NSMutableDictionary dictionary]; 163 | self.visibleViews = [NSMutableDictionary dictionary]; 164 | self.viewKeysToRemove = [NSMutableArray array]; 165 | self.indexToRectMap = [NSMutableDictionary dictionary]; 166 | } 167 | return self; 168 | } 169 | 170 | - (void)dealloc { 171 | // clear delegates 172 | self.delegate = nil; 173 | self.collectionViewDataSource = nil; 174 | self.collectionViewDelegate = nil; 175 | } 176 | 177 | #pragma mark - DataSource 178 | 179 | - (void)reloadData { 180 | [self relayoutViews]; 181 | } 182 | 183 | #pragma mark - View 184 | 185 | - (void)layoutSubviews { 186 | [super layoutSubviews]; 187 | 188 | UIInterfaceOrientation orientation = [UIApplication sharedApplication].statusBarOrientation; 189 | 190 | if (self.orientation != orientation) { 191 | self.orientation = orientation; 192 | // Recalculates layout 193 | [self relayoutViews]; 194 | } else if(self.lastWidth != self.width) { 195 | // Recalculates layout 196 | [self relayoutViews]; 197 | } else { 198 | // Recycles cells 199 | CGFloat diff = fabsf(self.lastOffset - self.contentOffset.y); 200 | 201 | if (diff > self.offsetThreshold) { 202 | self.lastOffset = self.contentOffset.y; 203 | 204 | [self removeAndAddCellsIfNecessary]; 205 | } 206 | } 207 | 208 | self.lastWidth = self.width; 209 | } 210 | 211 | - (void)relayoutViews { 212 | self.numCols = UIInterfaceOrientationIsPortrait(self.orientation) ? self.numColsPortrait : self.numColsLandscape; 213 | 214 | // Reset all state 215 | [self.visibleViews enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { 216 | PSCollectionViewCell *view = (PSCollectionViewCell *)obj; 217 | [self enqueueReusableView:view]; 218 | }]; 219 | [self.visibleViews removeAllObjects]; 220 | [self.viewKeysToRemove removeAllObjects]; 221 | [self.indexToRectMap removeAllObjects]; 222 | 223 | // This is where we should layout the entire grid first 224 | NSInteger numViews = [self.collectionViewDataSource numberOfRowsInCollectionView:self]; 225 | 226 | CGFloat totalHeight = 0.0; 227 | CGFloat top = kMargin; 228 | 229 | // Add headerView if it exists 230 | if (self.headerView) { 231 | top = self.headerView.top; 232 | self.headerView.width = self.width; 233 | [self addSubview:self.headerView]; 234 | top += self.headerView.height; 235 | } 236 | 237 | if (numViews > 0) { 238 | // This array determines the last height offset on a column 239 | NSMutableArray *colOffsets = [NSMutableArray arrayWithCapacity:self.numCols]; 240 | for (int i = 0; i < self.numCols; i++) { 241 | [colOffsets addObject:[NSNumber numberWithFloat:top]]; 242 | } 243 | 244 | // Calculate index to rect mapping 245 | self.colWidth = floorf((self.width - kMargin * (self.numCols + 1)) / self.numCols); 246 | for (NSInteger i = 0; i < numViews; i++) { 247 | NSString *key = PSCollectionKeyForIndex(i); 248 | 249 | // Find the shortest column 250 | NSInteger col = 0; 251 | CGFloat minHeight = [[colOffsets objectAtIndex:col] floatValue]; 252 | for (int i = 1; i < [colOffsets count]; i++) { 253 | CGFloat colHeight = [[colOffsets objectAtIndex:i] floatValue]; 254 | 255 | if (colHeight < minHeight) { 256 | col = i; 257 | minHeight = colHeight; 258 | } 259 | } 260 | 261 | CGFloat left = kMargin + (col * kMargin) + (col * self.colWidth); 262 | CGFloat top = [[colOffsets objectAtIndex:col] floatValue]; 263 | CGFloat colHeight = [self.collectionViewDataSource collectionView:self heightForRowAtIndex:i]; 264 | 265 | CGRect viewRect = CGRectMake(left, top, self.colWidth, colHeight); 266 | 267 | // Add to index rect map 268 | [self.indexToRectMap setObject:NSStringFromCGRect(viewRect) forKey:key]; 269 | 270 | // Update the last height offset for this column 271 | CGFloat heightOffset = colHeight > 0 ? top + colHeight + kMargin : top; 272 | 273 | [colOffsets replaceObjectAtIndex:col withObject:[NSNumber numberWithFloat:heightOffset]]; 274 | } 275 | 276 | for (NSNumber *colHeight in colOffsets) { 277 | totalHeight = (totalHeight < [colHeight floatValue]) ? [colHeight floatValue] : totalHeight; 278 | } 279 | } else { 280 | totalHeight = self.height; 281 | } 282 | 283 | // Add footerView if exists 284 | if (self.footerView) { 285 | self.footerView.top = totalHeight; 286 | self.footerView.width = self.width; 287 | [self addSubview:self.footerView]; 288 | totalHeight += self.footerView.height; 289 | } 290 | 291 | self.contentSize = CGSizeMake(self.width, totalHeight); 292 | 293 | [self removeAndAddCellsIfNecessary]; 294 | 295 | [[NSNotificationCenter defaultCenter] postNotificationName:kPSCollectionViewDidRelayoutNotification object:self]; 296 | } 297 | 298 | - (void)removeAndAddCellsIfNecessary { 299 | static NSInteger bufferViewFactor = 8; 300 | static NSInteger topIndex = 0; 301 | static NSInteger bottomIndex = 0; 302 | 303 | NSInteger numViews = [self.collectionViewDataSource numberOfRowsInCollectionView:self]; 304 | 305 | if (numViews == 0) return; 306 | 307 | // NSLog(@"diff: %f, lastOffset: %f", diff, self.lastOffset); 308 | 309 | // Find out what rows are visible 310 | CGRect visibleRect = CGRectMake(self.contentOffset.x, self.contentOffset.y, self.width, self.height); 311 | visibleRect = CGRectInset(visibleRect, 0, -1.0 * self.offsetThreshold); 312 | 313 | // Remove all rows that are not inside the visible rect 314 | [self.visibleViews enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { 315 | PSCollectionViewCell *view = (PSCollectionViewCell *)obj; 316 | CGRect viewRect = view.frame; 317 | if (!CGRectIntersectsRect(visibleRect, viewRect)) { 318 | [self enqueueReusableView:view]; 319 | [self.viewKeysToRemove addObject:key]; 320 | } 321 | }]; 322 | 323 | [self.visibleViews removeObjectsForKeys:self.viewKeysToRemove]; 324 | [self.viewKeysToRemove removeAllObjects]; 325 | 326 | if ([self.visibleViews count] == 0) { 327 | topIndex = 0; 328 | bottomIndex = numViews; 329 | } else { 330 | NSArray *sortedKeys = [[self.visibleViews allKeys] sortedArrayUsingComparator:^(id obj1, id obj2) { 331 | if ([obj1 integerValue] < [obj2 integerValue]) { 332 | return (NSComparisonResult)NSOrderedAscending; 333 | } else if ([obj1 integerValue] > [obj2 integerValue]) { 334 | return (NSComparisonResult)NSOrderedDescending; 335 | } else { 336 | return (NSComparisonResult)NSOrderedSame; 337 | } 338 | }]; 339 | topIndex = [[sortedKeys objectAtIndex:0] integerValue]; 340 | bottomIndex = [[sortedKeys lastObject] integerValue]; 341 | 342 | topIndex = MAX(0, topIndex - (bufferViewFactor * self.numCols)); 343 | bottomIndex = MIN(numViews, bottomIndex + (bufferViewFactor * self.numCols)); 344 | } 345 | // NSLog(@"topIndex: %d, bottomIndex: %d", topIndex, bottomIndex); 346 | 347 | // Add views 348 | for (NSInteger i = topIndex; i < bottomIndex; i++) { 349 | NSString *key = PSCollectionKeyForIndex(i); 350 | CGRect rect = CGRectFromString([self.indexToRectMap objectForKey:key]); 351 | 352 | // If view is within visible rect and is not already shown 353 | if (![self.visibleViews objectForKey:key] && CGRectIntersectsRect(visibleRect, rect)) { 354 | // Only add views if not visible 355 | PSCollectionViewCell *newCell = [self.collectionViewDataSource collectionView:self cellForRowAtIndex:i]; 356 | newCell.frame = CGRectFromString([self.indexToRectMap objectForKey:key]); 357 | [self addSubview:newCell]; 358 | 359 | // Setup gesture recognizer 360 | if ([newCell.gestureRecognizers count] == 0) { 361 | PSCollectionViewTapGestureRecognizer *gr = [[PSCollectionViewTapGestureRecognizer alloc] initWithTarget:self action:@selector(didSelectView:)]; 362 | gr.delegate = self; 363 | [newCell addGestureRecognizer:gr]; 364 | newCell.userInteractionEnabled = YES; 365 | } 366 | 367 | [self.visibleViews setObject:newCell forKey:key]; 368 | } 369 | } 370 | } 371 | 372 | #pragma mark - Reusing Views 373 | 374 | - (PSCollectionViewCell *)dequeueReusableViewForClass:(Class)viewClass { 375 | NSString *identifier = NSStringFromClass(viewClass); 376 | 377 | PSCollectionViewCell *view = nil; 378 | if ([self.reuseableViews objectForKey:identifier]) { 379 | view = [[self.reuseableViews objectForKey:identifier] anyObject]; 380 | 381 | if (view) { 382 | // Found a reusable view, remove it from the set 383 | [[self.reuseableViews objectForKey:identifier] removeObject:view]; 384 | } 385 | } 386 | 387 | return view; 388 | } 389 | 390 | - (void)enqueueReusableView:(PSCollectionViewCell *)view { 391 | if ([view respondsToSelector:@selector(prepareForReuse)]) { 392 | [view performSelector:@selector(prepareForReuse)]; 393 | } 394 | view.frame = CGRectZero; 395 | 396 | NSString *identifier = NSStringFromClass([view class]); 397 | if (![self.reuseableViews objectForKey:identifier]) { 398 | [self.reuseableViews setObject:[NSMutableSet set] forKey:identifier]; 399 | } 400 | 401 | [[self.reuseableViews objectForKey:identifier] addObject:view]; 402 | 403 | [view removeFromSuperview]; 404 | } 405 | 406 | #pragma mark - Gesture Recognizer 407 | 408 | - (void)didSelectView:(UITapGestureRecognizer *)gestureRecognizer { 409 | NSString *rectString = NSStringFromCGRect(gestureRecognizer.view.frame); 410 | NSArray *matchingKeys = [self.indexToRectMap allKeysForObject:rectString]; 411 | NSString *key = [matchingKeys lastObject]; 412 | if ([gestureRecognizer.view isMemberOfClass:[[self.visibleViews objectForKey:key] class]]) { 413 | if (self.collectionViewDelegate && [self.collectionViewDelegate respondsToSelector:@selector(collectionView:didSelectCell:atIndex:)]) { 414 | NSInteger matchingIndex = PSCollectionIndexForKey([matchingKeys lastObject]); 415 | [self.collectionViewDelegate collectionView:self didSelectCell:(PSCollectionViewCell *)gestureRecognizer.view atIndex:matchingIndex]; 416 | } 417 | } 418 | } 419 | 420 | - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch { 421 | if (![gestureRecognizer isMemberOfClass:[PSCollectionViewTapGestureRecognizer class]]) return YES; 422 | 423 | NSString *rectString = NSStringFromCGRect(gestureRecognizer.view.frame); 424 | NSArray *matchingKeys = [self.indexToRectMap allKeysForObject:rectString]; 425 | NSString *key = [matchingKeys lastObject]; 426 | 427 | if ([touch.view isMemberOfClass:[[self.visibleViews objectForKey:key] class]]) { 428 | return YES; 429 | } else { 430 | return NO; 431 | } 432 | } 433 | 434 | @end 435 | -------------------------------------------------------------------------------- /PSCollectionViewCell.h: -------------------------------------------------------------------------------- 1 | // 2 | // PSCollectionViewCell.h 3 | // 4 | // Copyright (c) 2012 Peter Stone (https://www.impeterstone.com) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | #import 25 | 26 | @class PSCollectionView; 27 | 28 | @interface PSCollectionViewCell : UIView 29 | 30 | @property (nonatomic, strong) id object; 31 | @property (nonatomic, weak) PSCollectionView *collectionView; 32 | @property (nonatomic, assign) NSInteger index; 33 | 34 | - (void)prepareForReuse; 35 | - (void)collectionView:(PSCollectionView *)collectionView fillCellWithObject:(id)object atIndex:(NSInteger)index; 36 | + (CGFloat)rowHeightForObject:(id)object inColumnWidth:(CGFloat)columnWidth; 37 | 38 | @end 39 | -------------------------------------------------------------------------------- /PSCollectionViewCell.m: -------------------------------------------------------------------------------- 1 | // 2 | // PSCollectionViewCell.m 3 | // 4 | // Copyright (c) 2012 Peter Stone (https://www.impeterstone.com) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | #import "PSCollectionViewCell.h" 25 | 26 | @interface PSCollectionViewCell () 27 | 28 | @end 29 | 30 | @implementation PSCollectionViewCell 31 | 32 | - (id)initWithFrame:(CGRect)frame { 33 | self = [super initWithFrame:frame]; 34 | if (self) { 35 | } 36 | return self; 37 | } 38 | 39 | 40 | - (void)prepareForReuse { 41 | } 42 | 43 | - (void)collectionView:(PSCollectionView *)collectionView fillCellWithObject:(id)object atIndex:(NSInteger)index { 44 | self.collectionView = collectionView; 45 | self.object = object; 46 | self.index = index; 47 | } 48 | 49 | + (CGFloat)rowHeightForObject:(id)object inColumnWidth:(CGFloat)columnWidth { 50 | return 0.0; 51 | } 52 | 53 | @end 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | What is PSCollectionView? 2 | --- 3 | It's a Pinterest style scroll view designed to be used similar to a UITableView. 4 | 5 | It supports Portrait and Landscape orientations. 6 | 7 | I built this as a hack to show my friends. Any suggestions or improvements are very welcome! 8 | 9 | Coming soon... A fully functional demo app. 10 | 11 | What is PSCollectionViewCell? 12 | --- 13 | It's the equivalent of UITableViewCell 14 | 15 | It implements base methods for configuring data and calculating height 16 | 17 | *Note: You should subclass this!* 18 | 19 | Want to see this code in a live app? 20 | --- 21 | I use this in my iPhone/iPad app, Lunchbox. 22 | 23 | ![Screenshot](http://a5.mzstatic.com/us/r1000/086/Purple/v4/b7/08/bb/b708bb3f-0775-67af-6765-e9f17e7384c4/mza_6463307710579208032.480x480-75.jpg) 24 | 25 | ARC 26 | --- 27 | Fully ARC compatible now 28 | 29 | How to use: 30 | --- 31 | Here's an example of creating an instance of PSCollectionView 32 | 33 | self.collectionView = [[PSCollectionView alloc] initWithFrame:CGRectZero]; 34 | self.collectionView.delegate = self; // This is for UIScrollViewDelegate 35 | self.collectionView.collectionViewDelegate = self; 36 | self.collectionView.collectionViewDataSource = self; 37 | self.collectionView.backgroundColor = [UIColor clearColor]; 38 | self.collectionView.autoresizingMask = ~UIViewAutoresizingNone; 39 | 40 | **Setting number of columns** 41 | 42 | // Specify number of columns for both iPhone and iPad 43 | if (isDeviceIPad()) { 44 | self.collectionView.numColsPortrait = 3; 45 | self.collectionView.numColsLandscape = 4; 46 | } else { 47 | self.collectionView.numColsPortrait = 1; 48 | self.collectionView.numColsLandscape = 2; 49 | } 50 | 51 | **Add a header view** 52 | 53 | UIView *headerView = ... 54 | self.collectionView.headerView = headerView; 55 | 56 | **Add a footer view** 57 | 58 | UIView *footerView = ... 59 | self.collectionView.footerView = footerView; 60 | 61 | **Reloading Data** 62 | [self.collectionView reloadData]; 63 | 64 | **Delegate and DataSource** 65 | 66 | - (Class)collectionView:(PSCollectionView *)collectionView cellClassForRowAtIndex:(NSInteger)index { 67 | return [PSCollectionViewCell class]; 68 | } 69 | 70 | - (NSInteger)numberOfRowsInCollectionView:(PSCollectionView *)collectionView { 71 | return 0; 72 | } 73 | 74 | - (UIView *)collectionView:(PSCollectionView *)collectionView cellForRowAtIndex:(NSInteger)index { 75 | return nil; 76 | } 77 | 78 | - (CGFloat)collectionView:(PSCollectionView *)collectionView heightForRowAtIndex:(NSInteger)index { 79 | return 0.0; 80 | } 81 | 82 | License 83 | --- 84 | Copyright (c) 2012 Peter Stone (https://www.impeterstone.com) 85 | 86 | Permission is hereby granted, free of charge, to any person obtaining a copy 87 | of this software and associated documentation files (the "Software"), to deal 88 | in the Software without restriction, including without limitation the rights 89 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 90 | copies of the Software, and to permit persons to whom the Software is 91 | furnished to do so, subject to the following conditions: 92 | 93 | The above copyright notice and this permission notice shall be included in 94 | all copies or substantial portions of the Software. 95 | 96 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 97 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 98 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 99 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 100 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 101 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 102 | THE SOFTWARE. 103 | 104 | Questions? 105 | --- 106 | Feel free to send me an email if you have any questions implementing PSCollectionView! 107 | --------------------------------------------------------------------------------