Storyboards, View Controller Containment and Delegation: Coordinating Responsibilities Within an App by Jake MacMullin

This year marks the 100th anniversary of Australia's capital city Canberra. During this centenary year CSIRO, Australia's peak scientific research body, wanted to reflect on their history in Canberra with an iPad and iPhone app which they asked Stripy Sock to create for them.

The app (available free from the app store) includes stories that can be viewed on a map or on a timeline. It is also a universal app with both an iPhone and iPad version. While both versions allow you to see the timeline and the map, they do so in different ways. On the iPad both the map and the timeline are presented side by side while on the iPhone you can switch between a timeline or map tab.

 

The iPad app showing a map and timeline

This post describes how I used storyboards, view controller containment and delegation so that these map and timeline components could be used in these different ways in both the iPhone and iPad apps.

As I knew that I'd be displaying stories on a map in both versions of the app, I wanted to make sure that any code I wrote in the first version I created (which was the iPad app) could be reused in the next version. But I also knew I wanted different behaviour in each version. Selecting a story on the map in the iPad app should cause the timeline visible alongside the map to scroll to the selected story so you can see both the time and place that relates to the story. However I knew the timeline wouldn't be visible alongside the map on the iPhone. As such I needed to develop the map as a distinct self-contained thing separate from the timeline.

After thinking it through, I determined the app would consist of:

  • A map view controller
  • A timeline view controller
  • A story detail view controller
  • And two container view controllers for the iPhone and iPad versions

The map view controller would be responsible for displaying the list of stories it was given on a map.

The timeline view controller would be responsible for displaying the list of stories it was given on a timeline.

In order to synchronise the map and timeline views in the iPad version I also needed some way to highlight a story on the map or in the timeline when the same story was selected in the other view. However, as explained previously I didn't want to tightly couple the timeline to the map as I knew the two wouldn't appear alongside each other in the iPhone version of the app.

Highlighting a given story was the obvious bit. Both my map and timeline view controllers would need methods to highlight a given story:

// IndexMapViewController.h


@interface IndexMapViewController : UIViewController

/**
* Scrolls the map to the given story and displays its annotation.
*/
- (void)selectStory:(Story *)story;

@end


@interface StoryTimelineViewController : UICollectionViewController

/**
* Scrolls the timeline to the given story and changes the
* style of the story's cell to indicate it is selected.
* Pass nil to clear the selection.
*/
- (void)selectStory:(Story *)story;

@end

Slightly less obvious was how to ensure the timeline's setStory: is called when a story is selected in the map view and vice versa. This is where delegation comes in. I know that I want the map view to inform some other class when a story is selected (though at this stage of the design of the app I wasn't entirely clear which class that'd be) so using a delegate is ideal. It let's me clearly define the different responsibilities. It's a way of saying, if you're interested in knowing when a story is selected, register your interest with me and I'll tell you. In this way, the map view controller's responsibilities are expanded beyond simply showing the list of stories to also include informing an interested delegate when a story is selected. It's not the map view controller's responsibility to do anything about the fact a story was selected, it simply has to inform its delegate.

// IndexMapViewController.h


@protocol IndexMapViewControllerDelegate <NSObject>

- (void)indexMapViewController:(IndexMapViewController *)controller didSelectStory:(Story *)story;

@end

Likewise, the timeline view controller has a similar delegate protocol:

// StoryTimelineViewController.h


@protocol StoryTimelineViewControllerDelegate <NSObject>

- (void)storyTimelineDidSelectStory:(Story *)story;

@end

The final piece of the puzzle is finding a class to take responsibility for acting as the map and timeline view controllers delegate to coordinate the two.

// IndexViewController.h

@interface IndexViewController : UIViewController <IndexMapViewControllerDelegate, StoryTimelineViewControllerDelegate>
@end

Now the division of responsibilities is clear. The map and timeline view controllers take responsibility for displaying the list of stories they've been given and informing their delegate when a story has been selected. The index view controller is responsible for providing both with the list of stories to display and acts as the delegate for both, ensuring that when a story is selected in one it is also selected in the other. But how does all this look in the Storyboard? How does the index view controller obtain references to the map and timeline view controllers in order to pass them their list of stories and register as their delegate? It's all done through the magic of view controller containment.

A section of the storyboard showing an index view controller with container view controllers for the map and timeline view controllers.

In the storyboard, the index view controller's view has two Container View Controllers embedded within it. One for the map and one for the timeline.

However, whilst this provides an easy way to create the parent-child relationship between the index view controller and its two children, it doesn't actually create a reference between the classes. To actually obtain these references and pass messages and data between the view controllers you have to write a bit of code:

// IndexViewController.h

@interface IndexViewController : UIViewController <IndexMapViewControllerDelegate, StoryTimelineViewControllerDelegate>

@property (nonatomic, strong) IndexMapViewController *mapViewController;
@property (nonatomic, strong) StoryTimelineViewController *timelineViewController;

@end
// IndexViewController.m

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
// if this is an embed segue then keep a reference to the view controller
// that is being embeded
if ([segue.identifier isEqualToString:@"EmbedMap"]) {
IndexMapViewController *mapViewController = (IndexMapViewController *)segue.destinationViewController;
[self setMapViewController:mapViewController];
[self.mapViewController setDelegate:self];
}
if ([segue.identifier isEqualToString:@"EmbedTimeline"]) {
StoryTimelineViewController *timelineViewController = (StoryTimelineViewController *)segue.destinationViewController;
[self setTimelineViewController:timelineViewController];
[self.timelineViewController setDelegate:self];
}
}

The prepareForSegue: method is called when the view controller should prepare for a certain segue. When using segues this is your view controller's opportunity to do something before the app segues to a new view controller. Typically, this method is used to pass variables to view controllers you're transitioning to. However, it can also be used to obtain a reference to any child view controllers that you've added using container view controllers in your storyboard. The code here simply looks for the segue identifiers for the 'embed segues' used to embed the map and timeline view controllers. It then casts each segue's 'destinationViewController' to the type I know it must be before storing a reference to it and setting the IndexViewController as the delegate.

The final step is to implement the map and timeline view controller's delegate methods:

// IndexViewController.m

- (void)indexMapViewController:(IndexMapViewController *)controller didSelectStory:(Story *)story
{
[self.timelineViewController selectStory:story];
}

- (void)storyTimelineDidSelectStory:(Story *)story
{
[self.mapViewController selectStory:story];
}

This might seem a little more complex than simply allowing the map view controller to know about the timeline view controller and vice versa, but this approach offers the flexibility to use these components independently of one another. Creating the iPhone version of the app was straightforward as a result.

The iPhone app uses the same map view controller, but   this time it is contained within its own tab.

Linking to the App Store nicely by Jake MacMullin

You may have occasion to want to send people to Apple's App Store from within your app. Perhaps you're recommending another app or the user has indicated she'd like to rate your app. If you find yourself in this situation, here's how you can do it nicely.

In iOS6 Apple introduced an API for displaying an app store page from within your app. This is much nicer than using a URL that causes your app to close and the App Store app to open and it is much, much nicer than using a URL that causes Safari to open momentarily before causing the App Store app to open.

Here's what you need to do. 

1. Add the StoreKit framework to your project. 

2. Initialise an SKStoreProductViewController, configure the delegate and ask it to load the details of the product you want to show to your user. 

 

SKStoreProductViewController *storeViewController = [[SKStoreProductViewController alloc] init];
[storeViewController setDelegate:self];

NSDictionary *productParams = @{ SKStoreProductParameterITunesItemIdentifier : @"401778175" };
[storeViewController loadProductWithParameters:productParams completionBlock:^(BOOL result, NSError *error) {
if (result == YES) {
[self presentModalViewController:storeViewController animated:YES];
} else {
// handle the error
}
}];

3. Finally, make sure you implement the SKStoreProductViewController's required delegate method.

 

- (void)productViewControllerDidFinish:(SKStoreProductViewController *)viewController {
[viewController dismissModalViewControllerAnimated:YES];
}

That's it. Now when your user has finished looking at the app you're showing her she can return to whatever she was doing in your app with a single tap.

National Library of Australia's Forte by Jake MacMullin

The National Library of Australia has an incredible collection of Australian sheet music. There are over 13,000 individual items in the digitised sheet music collection which is housed in the Library in Canberra. Whilst this collection is also available on the Library's web site, until now the experience of browsing the digitised music in the collection has been nothing like that of flipping through a physical score. 

Stripy Sock helped create an iPad app to make exploring the Library's digitised collection of sheet music more like ​the experience of handling the 'real thing'. The app lets you browse through thousands of items of music in an instant and allows you to share your favourite discoveries via Email, Facebook or Twitter.

You can download the iPad app for free from Apple's App Store or find out more about the app and the collection on the National Library's web site.

The app is also open source and you can access the source code at GitHub.​

Working around a bug(?) in UICollectionView by Jake MacMullin

I think I've just found a bug in UICollectionView. Specifically in how it informs you about the currently visible cells.​

Here's the bug report I just submitted to Apple (and Open Radar):​

<bug report>​

Summary:

UICollectionView's visibleCells and indexPathsForVisibleItems return arrays of items that are in the wrong order.

I have a UICollectionView using the default layout that is configured in my Storyboard to display a single row of items scrolling horizontally. I have implemented the UICollectionView's scrollViewDidScroll delegate method. In my implementation of this method I want to determine the 'first visible item' in order to display some information about this item in another part of my app.

To determine the 'first visible item' I ask the collection view for it's visibleCells or the indexPathsForVisibleItems. Given these methods return an ordered array and that the documentation doesn't state otherwise, I'd expect the order of the items in these arrays to match the order of the cells in my collection view. However, in both cases the order of the items that are returned do not always match the order of those items within the collection view.

Steps to Reproduce:

  1. Create a UICollectionView with some items.
  2. In the collection view's delegate implement the scrollViewDidScroll method.
  3. Ask the collection view for its visibleCells and indexPathsForVisibleItems.
  4. Examine the order of the items that are returned.
  5. Scroll the collection view so that cells are reused.
  6. Examine the order of the items that are returned by visibleCells and indexPathsForVisibleItems again.

Expected Results:

The collection view's visibleCells and indexPathsForVisibleItems return arrays of items whose order matches the order of the cells within the collection view.

Actual Results:

Before any cells are reused both methods return arrays of items whose order does match the order of the cells in the collection view - eg.

indexPathsForVisibleItems:
<__NSArrayM 0x756ed60>(
<NSIndexPath 0x817e470> 2 indexes [0, 0],
<NSIndexPath 0x817e5f0> 2 indexes [0, 1],
<NSIndexPath 0x817e6a0> 2 indexes [0, 2],
<NSIndexPath 0x817e750> 2 indexes [0, 3]
)

visibleCells:
<__NSArrayM 0x756bad0>(
1 : <LabelCell: 0x81809f0; baseClass = UICollectionViewCell; frame = (0 32; 226 236); clipsToBounds = YES; opaque = NO; layer = <CALayer: 0x8180b20>>,
2 : <LabelCell: 0x8186eb0; baseClass = UICollectionViewCell; frame = (236 32; 226 236); clipsToBounds = YES; opaque = NO; layer = <CALayer: 0x8186f40>>,
3 : <LabelCell: 0x81886e0; baseClass = UICollectionViewCell; frame = (472 32; 226 236); clipsToBounds = YES; opaque = NO; layer = <CALayer: 0x8188680>>,
4 : <LabelCell: 0x81869c0; baseClass = UICollectionViewCell; frame = (708 32; 226 236); clipsToBounds = YES; opaque = NO; layer = <CALayer: 0x8186a50>>
)

After the collection view has scrolled enough to reuse a cell, the order of items returned by these methods no longer matches the order in which the items appear in the collection view. eg.

indexPathsForVisibleItems:
<__NSArrayM 0x756bad0>(
<NSIndexPath 0x71911a0> 2 indexes [0, 6],
<NSIndexPath 0x817e6a0> 2 indexes [0, 2],
<NSIndexPath 0x817e750> 2 indexes [0, 3],
<NSIndexPath 0x817e800> 2 indexes [0, 4],
<NSIndexPath 0x71b56e0> 2 indexes [0, 5]
)

visibleCells:
<__NSArrayM 0x7572450>(
7 : <LabelCell: 0x8186eb0; baseClass = UICollectionViewCell; frame = (1416 32; 226 236); clipsToBounds = YES; opaque = NO; layer = <CALayer: 0x8186f40>>,
3 : <LabelCell: 0x81886e0; baseClass = UICollectionViewCell; frame = (472 32; 226 236); clipsToBounds = YES; opaque = NO; layer = <CALayer: 0x8188680>>,
4 : <LabelCell: 0x81869c0; baseClass = UICollectionViewCell; frame = (708 32; 226 236); clipsToBounds = YES; opaque = NO; layer = <CALayer: 0x8186a50>>,
5 : <LabelCell: 0x71b1910; baseClass = UICollectionViewCell; frame = (944 32; 226 236); clipsToBounds = YES; opaque = NO; layer = <CALayer: 0x7180140>>,
6 : <LabelCell: 0x81809f0; baseClass = UICollectionViewCell; frame = (1180 32; 226 236); clipsToBounds = YES; opaque = NO; layer = <CALayer: 0x8180b20>>
)

Notes:

I've attached an Xcode project that demonstrates this bug.

If the API is behaving as it was designed to, then I think either the signature of these methods should change so that they return an unordered set rather than an ordered array or the documentation should be updated to make it clear not to rely on the order of items returned by these methods.

</bug report>​

It could be that I'm just mis-understanding the API. That's partly why I'm posting this here too. If I'm doing something wrong, please let me know in the comments below!

Alternatively, this is a real bug and I have to work around it until it is fixed. Here's my simple workaround if you encounter this yourself. I simply use the 'indexPathsForVisibleItems' method that returns an array of index paths and then I sort that array by the index paths. Typical bug: it took hours to figure out what was going wrong and minutes to work-around once I knew where I'd gone wrong.

    NSArray *indexPathsOfVisibleStories = [self.collectionView indexPathsForVisibleItems];
NSArray *sortedIndexPaths = [indexPathsOfVisibleStories sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) {
NSIndexPath *path1 = (NSIndexPath *)obj1;
NSIndexPath *path2 = (NSIndexPath *)obj2;
return [path1 compare:path2];
}];

You can download the sample Xcode project if you're interested in seeing this bug in action.

Creating a custom collection view layout by Jake MacMullin

Apple introduced a significant new class in iOS6 that makes it easier to create a range of different user interfaces: collection views.​ Up until iOS6 the main class used to display a series of items was the table view. Despite its name, it doesn't support tabular information at all, but is really designed to display vertically scrolling lists of content. Whilst you can do a lot with a vertically scrolling list of content they have their limitations. No horizontal scrolling, only one item per row, no complex layouts. Prior to iOS6 if you wanted to do any of these things, you were on your own.

In iOS6 Apple introduced collection views which address all of these limitations of table views and make it much easier to display content in different ways.​

Where table views were very narrowly defined, collection views are designed to be flexible. They can be used to display collections of content in a variety of different ways: horizontal or vertically scrolling, lists or grids or any other layout you want. They provide this flexibility by cooperating with collection view layouts to determine where each item in the collection view should appear. Apple have provided a single example of a layout that will meet many people's needs. The flow layout is a line-based layout for collections. Each item is displayed next to the previous item along a line until it reaches the end of a line and then items are positioned on the next line. Depending on how this is configured it can be used to display vertically or horizontally scrolling lists (every item on a single line) or grids (multiple items per line).

Even if your layout isn't as simple as this you can still use a collection view. You just need to provide your own layout. This post describes the process for creating such a custom layout.​ I'm not trying to provide a complete explanation of collection views or collection view layouts, Apple's documentation and WWDC videos (sessions 205 and 219) are the best place for that. Rather, this post aims to be a 'worked example' of creating a custom layout for one specific set of requirements.

An app I'm working on at the moment requires a collection of content to be displayed in a series of lines. If there's more content than will fit in a single view, users will be able to scroll horizontally to see more content. Sounds like just the thing for a collection view. At first glance it seems like the standard flow layout would be ideal too.​

However, there is an important difference that means I'll need to develop my own custom layout: some cells are bigger than others and span multiple lines.

​A more complex layout where some cells span multiple lines.

To support this kind of layout, I'll need to provide an implementation of UICollectionViewLayout that can tell the collection view where each item should be displayed.

The first thing my layout will need to do is to tell the collection view how big the content size is. My layout will display up to 12 items per 'page'​ and will scroll horizontally if there's more than one page. So calculating the content size is simply a matter of asking the collection view's data source how many items need to be displayed and then determining how many pages will be needed to display them all. I do this by implementing the -collectionViewContentSize method:

- (CGSize)collectionViewContentSize
{
// Ask the data source how many items there are (assume a single section)
id<UICollectionViewDataSource> dataSource = self.collectionView.dataSource;
int numberOfItems = [dataSource collectionView:self.collectionView numberOfItemsInSection:0];

// Determine how many pages are needed
int numberOfPages = ceil((float)numberOfItems / (float)kNumberOfItemsPerPage);

// Set the size
float pageWidth = self.collectionView.frame.size.width;
float pageHeight = self.collectionView.frame.size.height;
CGSize contentSize = CGSizeMake(numberOfPages * pageWidth, pageHeight);

return contentSize;
}

The next thing my custom layout needs to be able to do is to specify the position and size for each item. For each item, my layout will need to figure out: 

  • which page the item is on,
  • which row the item is on (or what its vertical offset is), 
  • which position the item is in the given row (or what its horizontal offset is)​ and
  • its size.

The collection view will ask the layout for this information in two different ways. When it needs to display a new area of the collection, it will ask the layout for the -layoutAttributesForElementsInRect: passing a rect to define the region of the collection view it is interested in. It may also ask for layout information for a given element from time to time by calling -layoutAttributesForItemAtIndexPath: and pass an index path to indicate which element's layout information should be returned. For layouts that might be used to display a significant number of elements it is probably worth calculating this layout information lazily just for the elements within the specified rect. However, the app I'm working on will only have about 100 elements to layout, so for simplicity I'll calculate the layout attributes up-front and cache them. I can always optimise this later if it turns out to be a performance problem.​

According to Apple's documentation, layouts are initialised by calling the following methods in the following order:​

  1. prepareLayout
  2. collectionViewContentSize
  3. layoutAttributesForElementsInRect

So I'll calculate all the layout information in the initial -prepareLayout method. There may be a more elegant way of calculating this layout information but for now I'll use some simple logic:

Given that I know how many elements will be displayed on a given page (12), I can determine which page a given element appears on by dividing the index of the element in question by 12.​ I can then set an initial horizontal offset for that page by multiplying the page number by the page width (the collection view width). This means all I need to do is to determine how far the given element is from the edges of the current page. Given I know that there are 12 possible positions per page, I can determine which of those 12 positions the current element should occupy by finding the remainder left when I divide the index of the given element by 12. Once I know which position the current element should occupy all I need to do is look-up the horizontal and vertical offsets and the size. There's only 4 possible horizontal offsets for each page, 4 possible vertical offsets and 3 possible sizes so it is quite straight-forward to determine the correct values using some simple switch statements. I can then use these values to create a layout attributes object with an appropriate frame for the given element.

- (CustomFlowLayoutAttributes *)layoutAttributesForItemAtIndex:(int)index
{
NSIndexPath *path = [NSIndexPath indexPathForItem:index inSection:0];
CustomFlowLayoutAttributes *attributes = (CustomFlowLayoutAttributes *)[super layoutAttributesForItemAtIndexPath:path];

// Figure out what page this item is on
int pageNumber = floor((float)index / (float)kNumberOfItemsPerPage);

// Set the horizontal offset for the start of the page
float pageWidth = self.collectionView.frame.size.width;
float horizontalOffset = pageNumber * pageWidth;

// Now, determine which position this cell occupies on the page.
int indexOnPage = index % kNumberOfItemsPerPage;

int column = 0;
switch (indexOnPage) {
case 0:
case 3:
case 5:
column = 0;
break;
case 1:
case 4:
case 6:
case 9:
column = 1;
break;
case 2:
case 7:
case 10:
column = 2;
break;
case 8:
case 11:
column = 3;
break;
default:
column = 0;
break;
}

int row = 0;
switch (indexOnPage) {
case 0:
case 1:
case 2:
row = 0;
break;
case 3:
case 4:
row = 1;
break;
case 5:
case 6:
case 7:
case 8:
row = 2;
break;
case 9:
case 10:
case 11:
row = 3;
break;
default:
row = 0;
break;
}

horizontalOffset = horizontalOffset + ((kColumnWidth + kPadding) * column);
float verticalOffset = (kRowHeight + kPadding) * row;

// finally, determine the size of the cell.
float width = 0.0;
float height = 0.0;

switch (indexOnPage) {
case 2:
width = kLargeCellWidth;
height = kLargeCellHeight;
break;
case 5:
width = kColumnWidth;
height = kMediumCellHeight;
break;
default:
width = kColumnWidth;
height = kRowHeight;
break;
}

CGRect frame = CGRectMake(horizontalOffset, verticalOffset, width, height);
[attributes setFrame:frame];

return attributes;
}

Having calculated the layout attributes up-front, I can easily return the appropriate ones when asked in ​layoutAttributesForItemAtIndexPath: or layoutAttributesForElementsInRect.

That's all I need to so that my custom layout can be used by a collection view. It'll allow the collection view to display the right number of cells of the right size in the right place. There's just one more small thing I can do to simplify things when it comes time to implement the display for my cells. Given that there are three quite different cell frames (a small landscape, a large landscape and a medium sized portrait) the cell's code will most likely need to be able to differentiate between the three different frames in order to request and display an appropriately sized image in the cell (for example). Rather than write a bunch of logic in my cell's class, I can get my custom layout to add some additional information to the layout attributes that it passes the cell.​

To do this I need to:​

Extend UICollectionViewLayoutAttributes to create a custom sub-class that has properties with the extra information I want my layout to pass to my cells (a cell type in this case).​ In addition to creating the extra property, I also need to override the superclass's implementation of copyWithZone: to make sure the additional property is copied as UICollectionView uses the NSCopying protocol to create copies of the properties to pass to the cells.

Implement a class method in my custom layout to specify the ​UICollectionViewLayoutAttributes sub-class that should be returned when my layout is asked for the layout attributes.

+ (Class)layoutAttributesClass
{
return [CustomFlowLayoutAttributes class];
}

Then I just add the additional information when I'm calculating the layout attributes and implement some logic in my UICollectionViewCell subclass to receive the layout attributes and do something useful with the extra information my layout is providing.

​Whilst Apple's documentation (and countless mentions in their WWDC videos) make it clear that for most people they think the standard flow layout should be sufficient, should you find yourself in a situation where the flow layout doesn't quite meet your needs creating a custom layout isn't too much work.