6

I have an UICollectionView with three different prototype cells, each of which with different heights set via Storyboard. During runtime, the Collection View uses its own cell size, ignoring my Storyboard ones.

I am currently using collectionView:layout:sizeForItemAtIndexPath: with a couple conditionals to set each CGSize straight.

Is there a better way to set the cell sizes? I don't seem to be able to retrieve the Storyboard size each cell has, and CGSizeMake seems too hardcoded and not really flexible.

Uzaak
  • 578
  • 3
  • 13

3 Answers3

2

It seems that there's currently no easy way to:

  • Fetch UICollectionViewCell prototype cell sizes runtime from Storyboard(s).
  • Manage sizes of prototype cells just in one place (rather than having to enter them in the Storyboard cell prototype and implement sizeForItemAtIndexPath).

A method proposed here (for UITableViews) does not work, because using dequeueReusableCellWithReuseIdentifier in sizeForItemAtIndexPath will cause an indefinite loop.

However, I've managed to do this the following way:

  1. Add a unique (across all UICollectionViewCells in every storyboard) reuse identifier into each of your UICollectionView prototype cells in all Storyboards.

  2. Add a Run script Build Phase to your project with the script that pulls out UICollectionViewCell frame sizes from all Storyboards.

    output=${PROJECT_DIR}/StoryboardPrototypeCellSizes.h
    printf "@{" > $output
    
    for storyboard in $(find ${PROJECT_DIR} -name "*.storyboard")
    do
        echo "Scanning storyboard $storyboard..."
        delimiter=
        for line in $(xpath $storyboard "//collectionViewCell/@reuseIdentifier[string-length()>0] | //collectionViewCell/rect" 2>&-)
        do
            case $line in
                reuseIdentifier*)
                    reuseIdentifier=$(sed 's/[^"]*"\([^"]*\)".*/\1/' <<< $line)
                    ;;
                width*)
                    if [ -n "$reuseIdentifier" ]; then
                        width=$(sed 's/[^"]*"\([^"]*\)".*/\1/' <<< $line)
                    fi
                    ;;
                height*)
                    if [ -n "$reuseIdentifier" ]; then
                        height=$(sed 's/[^"]*"\([^"]*\)".*/\1/' <<< $line)
                    fi
                    ;;
            esac
    
            if [ -n "$reuseIdentifier" ] && [ -n "$width" ] && [ -n "$height" ]; then
                printf "$delimiter@\"$reuseIdentifier\" : [NSValue valueWithCGSize:CGSizeMake($width, $height)]" >> $output
                unset reuseIdentifier
                unset width
                unset height
                delimiter=,\\n
            fi
        done
    done
    
    printf "};\n" >> $output
    

    This creates a header file called StoryboardPrototypeCellSizes.h with a following example content:

    @{@"TodayCell" : [NSValue valueWithCGSize:CGSizeMake(320, 80)],
    @"SpecialDayCell" : [NSValue valueWithCGSize:CGSizeMake(320, 42)],
    @"NameDayCell" : [NSValue valueWithCGSize:CGSizeMake(320, 30)]};
    
  3. Add a helper method to return the UICollectionViewCell reuse identifier in the view controller controlling your UICollectionView:

    - (NSString *)cellReuseIdentifierAtIndexPath:(NSIndexPath *)indexPath
    {
        switch (indexPath.item) {
            case 0: return @"TodayCell";
            case 1: return @"SpecialDayCell";
            case 2: return @"NameDayCell";
        }
        return nil;
    }
    
  4. Be sure to use the same reuse identifier in cellForItemAtIndexPath:

    - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
    {
        UICollectionViewCell *cell =
            [collectionView dequeueReusableCellWithReuseIdentifier:
                [self cellReuseIdentifierAtIndexPath:indexPath]
                forIndexPath:indexPath];
        ...
    
  5. Finally implement sizeForItemAtIndexPath:

    - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
    {
        NSDictionary *storyboardPrototypeCellSizes =
    #import "StoryboardPrototypeCellSizes.h"
    
         return [(NSValue *)storyboardPrototypeCellSizes[
                 [self cellReuseIdentifierAtIndexPath:indexPath]
                ] CGSizeValue];
    }
    

This solution allows you to define UICollectionViewCell prototype cell sizes only once in the Storyboard(s) and also doesn't do any non-App-Store-compliant at runtime.

****Edit:**** You can also fetch UICollectionReusableView sizes by adding another script with the same content and replacing "collectionViewCell" with "collectionReusableView", and renaming the header file to, for example, StoryboardReusableViewSizes.h

Community
  • 1
  • 1
Markus Rautopuro
  • 7,997
  • 6
  • 47
  • 60
  • Hella cheating, but... brilliant. – i_am_jorf Aug 28 '14 at 21:17
  • 1
    For those coming here after the release of iOS 8, the solution to this problem would be using self-sizing cells. By autolayouting the cells making their width/height variable depending on the content size, it is possible to have the CollectionView resize the cells to fit their content inside. – Uzaak Mar 30 '15 at 15:22
  • 'A method proposed here (for UITableViews) does not work, because using dequeueReusableCellWithReuseIdentifier in sizeForItemAtIndexPath will cause an indefinite loop.' This one really helped me out, since i got no explanation why my app always crashed. – Maverick1st Mar 06 '17 at 12:22
0

There is no better way to set cell sizes. Cell sizes are used in several places in UICollectionView - for positioning, for scroll indicator. And it is very important to receive them as quick as possible in case if user scrolls collection with thousands small cells for example. So create cell and ask it about its size is not an option. You have to implement collectionView:layout:sizeForItemAtIndexPath: and it should work quickly.

Avt
  • 16,927
  • 4
  • 52
  • 72
-1

Are you using flow layout in your UICollectionView? If yes, you can use sizeForItemAtIndexPath method of the UICollectionViewDelegateFlowLayout protocol to provide the size of a cell. If you don't have issues using OSS components in your app, RFQuiltLayout can be used to achieve this.

indyfromoz
  • 4,045
  • 26
  • 24
  • Like I mentioned on the question itself, I am currently using sizeForItemAtIndexPath. Also, I might be wrong but doesn't RFQuiltLayout divide the collection in small blocks? I don't see it fit for use with a couple of variable height cells which are NOT proportional (say, one is 90 points, the other is 131 points, and the last one is 78 points high). – Uzaak Mar 18 '14 at 02:34
  • You mention there are three different prototype cells in your `UICollectionView`, have you explored laying out their sub-views with Autolayout? Obc.io has a [nice article](http://www.objc.io/issue-3/collection-view-layouts.html) on `UICollectionView` layouts. A sample on using Autolayout in UICollectionView is [here](https://www.cocoanetics.com/2013/08/variable-sized-items-in-uicollectionview/). In my experience, it is best to use the layout object of the `UICollectionView` to control the size and position of each cell while using Autolayout to layout the subviews? – indyfromoz Mar 18 '14 at 02:57
  • We're getting closer to the outcome I want! That's one great article. I was trying something exactly along these lines, but I was looking for a way to use the Storyboard cells I instantiated inside the Storyboard Collection View, instead of creating a XIB outside and registering them programatically. Any ideas left? Edit: I tried instantiating them via Tag numbers, but that doesn't seem to work. Also, I can't dequeue inside sizeForItem. – Uzaak Mar 18 '14 at 04:40