Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UICollectionView: How to get item size for a specific item after setting them in delegate method

I am fiddling with the new UICollectionView and the UICollectionViewLayout classes. I have created a custom layout, subclassing UICollectionViewFlowLayout.

My cell sizes are changing dynamically and I set the item sizes using the delegate method below

- (CGSize)collectionView:(UICollectionView *)collectionView
                  layout:(UICollectionViewLayout*)collectionViewLayout
  sizeForItemAtIndexPath:(NSIndexPath *)indexPath {

      NSLog(@"SETTING SIZE FOR ITEM AT INDEX %d", indexPath.row);
      return CGSizeMake(80, 80);
}

Now, under the prepareLayout method of my custom UICollectionViewFlowLayout class, I need to access these size variables so that I can make calculations how to place them and cache them for layoutAttributesForItemAtIndexPath.

However, I can't seem to find any property under UICollectionView or UICollectionViewFlowLayout to reach the custom item sizes I set in the delegate method.

like image 470
SarpErdag Avatar asked Sep 25 '12 14:09

SarpErdag


2 Answers

Found it myself.

Implement the custom class like without omitting UICollectionViewDelegateFlowLayout

@interface SECollectionViewCustomLayout : UICollectionViewFlowLayout 
                                          <UICollectionViewDelegateFlowLayout>

and then you can call

CGSize size = [self collectionView:self.collectionView 
                            layout:self 
            sizeForItemAtIndexPath:indexPath];
like image 96
SarpErdag Avatar answered Sep 20 '22 05:09

SarpErdag


Looking at the various UICollectionView... header files, and watching the WWDC 2012 Session 219 - Advanced Collection Views and Building Custom Layouts video (from about 6:50 onwards), it seems the extensible delegate pattern takes advantage of dynamic typing to ensure the layout can properly access its extended delegate methods.


In short...

  1. If you define a custom layout with its own delegate, define that delegate protocol in the layout's header file.
  2. Your delegate object (typically the UI(Collection)ViewController that manages the collection view) should declare itself to support this custom protocol.
    • In the case that your layout is just a UICollectionViewFlowLayout or subclass thereof, this just means declaring conformance to UICollectionViewDelegateFlowLayout.
    • Feel free to do this in your class extension in the .m file if you'd rather not #import the layout header into the delegate's interface.
  3. To access the delegate methods from the layout, call through to the collection view's delegate.
    • Use the layout's collectionView property, and cast the delegate to an object conforming to the required protocol to convince the compiler.
    • Don't forget to check that the delegate respondsToSelector: as usual prior to calling optional delegate methods. In fact, if you like, there's no harm in doing this for all methods, as the typecasting means there is no runtime guarantee the delegate will even implement the required methods.


In code...

So if you implement a custom layout that requires a delegate for some of its information, your header might look something like this:

@protocol CollectionViewDelegateCustomLayout <UICollectionViewDelegate>
- (BOOL)collectionView:(UICollectionView *)collectionView
                layout:(UICollectionViewLayout *)layout
shouldDoSomethingMindblowingAtIndexPath:(NSIndexPath *)indexPath;
@end

@interface CustomLayout : UICollectionViewLayout
// ...
@end


Your delegate declares conformance (I've done so in the implementation file here):

#import "CustomLayout.h"

@interface MyCollectionViewController () <CollectionViewDelegateCustomLayout>
@end

@implementation
// ...
- (BOOL)collectionView:(UICollectionView *)collectionView
                layout:(UICollectionViewLayout *)layout
shouldDoSomethingMindblowingAtIndexPath:(NSIndexPath *)indexPath
{
    return [self canDoSomethingMindblowing];
}
// ...
@end


And in your layout's implementation, you access the method like this:

BOOL blowMind;
if ([self.collectionView.delegate respondsToSelector:@selecor(collectionView:layout:shouldDoSomethingMindblowingAtIndexPath:)]) {
    blowMind = [(id<CollectionViewDelegateCustomLayout>)self.collectionView.delegate collectionView:self.collectionView
                                                                                             layout:self
                                                            shouldDoSomethingMindblowingAtIndexPath:indexPath];
} else {
    // Perhaps the layout also has a property for this, if the delegate
    // doesn't support dynamic layout properties...?
    // blowMind = self.blowMind;
}

Note that it's safe to typecast here, as we're checking the delegate responds to that method beforehand anyway.


The evidence...

It's only speculation, but I suspect it is how Apple manages the UICollectionViewDelegateFlowLayout protocol.

  • There is no delegate property on the flow layout, so calls must go via the collection view's delegate.
  • UICollectionViewController does not publicly conform to extended flow layout delegate (and I doubt it does so in another private header).
  • UICollectionView's delegate property only declares conformance to the 'base' UICollectionViewDelegate protocol. Again, I doubt there is a private subclass/category of UICollectionView in use by the flow layout to prevent the need for typecasting. To add further weight to this point, Apple discourages subclassing UICollectionView at all in the docs (Collection View Programming Guide for iOS: Creating Custom Layouts):

Avoid subclassing UICollectionView. The collection view has little or no appearance of its own. Instead, it pulls all of its views from your data source object and all of the layout-related information from the layout object.

So there we go. Not complicated, but worth knowing how to do it paradigm-friendly way.

like image 27
Stuart Avatar answered Sep 22 '22 05:09

Stuart