Pinterest-Style UICollectionViews in Xamarin.iOS
With the recent rise of Pinterest, waterfall-styled layouts have also seen a rise in popularity. Because I like to stay in style, I decided there was no reason I couldn’t use this design style in my Xamarin.iOS apps. However, there was a barrier. Apple doesn’t ship a layout like this. So naturally I scoured the web for some Objective-C layouts that I could bind so that I could play around with it in Xamarin.iOS. I eventually came across a library called CHTCollectionViewWaterfallLayout by Nelson. After thirty minutes or so spent on the binding process, I had this up and running on Xamarin’s tooling.
I liked the control so much that I decided to use it in my app and convert it 100% to C#. The result: WaterfallCollectionViewLayout - a simple layout for Xamarin.iOS that allows you to leverage the styling of the waterfall layout made famous by Pinterest.
You can download a release-ready version from Github, and view the documentation that accompanies the library. However, for your convenience, I will also walk you through the complete process of integrating WaterfallCollectionViewLayout into your app.
For starters, you will need to be familiar with UICollectionViews. They aren’t a particularly difficult topic to conquer (especially if you have worked with UITableViews). Additionally, you will need to download the latest release so that you can reference the assembly in your project. Finally, download the CollectionViewTemplate project that will serve as a base moving forward. There are several parts to a successful integration, so I will walk you through piece-by-piece.
A Walkthrough of CollectionViewTemplate
Open up the solution file for the CollectionViewTemplate. This project is a non-functional (yet) template for getting started with UICollectionViews. All of the necessary pieces for a regular UICollectionView are already in place, so all that needs to be handled is to add in the layout-related stuff. This is intended to teach you exactly what makes implementing a PBCollectionViewWaterfallLayout different than other layouts.
If you open up AppDelegate.cs, you will find the FinishedLaunching method, the entry point for our application. There are currently some unused fields I’ve defined, as well as some methods that we will fill in later.
Next, the Tag class that has been defined serves as a data model for our cell. There isn’t really anything special about it. The collection view will present a series of hashtags (represented by the Name property) with a corresponding image below (represented by the Image property).
WaterfallCell isn’t really that different from any cell that you may have come across, however, there is one small thing that makes it stand out. I’ll point that out later.
WaterfallDelegate will be the delegate for our layout that will be implemented at a later time.
Finally, WaterfallCollectionViewController is like any other UICollectionViewController, except I have added an UpdateLayout method, which I call in ViewDidAppear.
1. Populating Data & Calculating Heights
The first step to a successful implementation of WaterfallCollectionViewLayout is figuring out exactly how tall each cell should be. In this custom layout, the widths are a fixed height (to be set later), while the heights are flexible (hence: waterfall). However, the layout isn’t smart enough to know exactly how high each cell should be. This provides the user with more opportunity for customization.
For our app, we are going to want to access some data stored in a database or pull it from a web service. For simplicity’s sake, this data will generated in the DownloadData method of AppDelegate.cs. This data will be used by both the layout’s delegate, and the collection view’s cell to draw WaterfallCell.
Using the data field of AppDelegate as our object to which we are storing data, we can just mock up some quick fake data. Note: Images #1-3 are in the Resources folder, and were provided by the template. As I said earlier, pretend we are drawing this data from something like the Google or Bing Images API. Unfortunately, the images I’ve provided don’t really match the tag, but I suppose that’s not the point.
Next, we need to calculate the height for our cell using the information we find in each Tag object of the List<Tag>. Using a foreach loop inside of CalculateCellHeights, we will base the cell’s height off of the Image of the Tag object, and store it in the cellHeights field. This will serve as the height of each cell.
You may ask: isn’t this something we should do in the delegate class itself? You certainly can. The reason for me not doing so is that it slows the drawing process of the UICollectionView, which makes it look pretty laggy. I would rather have the heights pre-calculated, and pass that to the delegate. If this was a normal application with more views, I would probably download this data and calculate this asynchronously and store it until needed.
Finally, let’s setup the layout. To clean up the code a bit, I’m doing this inside of the SetupLayout method. Let’s start out by calling DownloadData as well as CalculateCellHeights. Once we have our data and heights, next setup the delegate by initializing the field with the new operator, and pass in the cellHeights into the delegate’s constructor (which we haven’t setup yet).
Next, we will setup the layout by doing the following:
Notice the four properties we set. ColumnCount, which defines the amount of columns the layout has is by default set to two. For good measure, I’ve also set it to two. ItemWidth is the width of each cell. As I noted earlier, one restriction with this layout is that widths are fixed. Finally, SectionInset provides some padding around the UICollectionView so that our cells aren’t drawn right on the edge.
Important: Keep a class-level reference to both the layout and the delegate. If you don’t, one of these (if not both) will be garbage collected, and your app will crash.
2. The Collection View Cell
I mentioned there was something special about WaterfallCell that makes it unique. That one thing can be found within the PopulateCell method when I set the PlaceholderImage’s frame to the height of the image. This auto-resizes the UIImageView so that it will match the value we set in our cellHeights field of the AppDelegate.
PlaceholderImage.Frame = new RectangleF (0, DisplayLabel.Bounds.Height, 129, image.Size.Height);
Of course, your cell may require no additional configuration changes. But if it does, remember that the delegate draws the cell of the appropriate size, it doesn’t resize content to the size you specify. That will need to be done here (unless you have AutoresizingMasks on).
3. The Layout Delegate
The delegate for the layout handles the heights of each cell, as mentioned earlier. Open up WaterfallDelegate.cs and add a “using WaterfallCollectionViewLayout;” compiler directive to the top of the file. This will allow us to access the custom layout delegate type.
Next, make WaterfallDelegate a subclass of PBCollectionViewDelegateWaterfallLayout and add a constructor that takes in a List<float> as a parameter and pass this to the base class. This will be where we can access the heights we calculated earlier. Add a class-level field of type List<float> that will store the incoming information from the constructor. Finally, override the GetHeightForCell method and return a float using the indexPath parameter. Your implementation should look something like this:
Obviously, this delegate could be implemented any number of ways just as long as the HeightForCell method returns a correct value for the corresponding cell.
4. The Collection View
Finally, we will make some changes to the WaterfallCollectionViewController class to make it waterfall-layout-ready. In the UpdateLayout method, insert the following code:
var layout = (PBCollectionViewWaterfallLayout)CollectionView.CollectionViewLayout;
layout.ColumnCount = (int) (CollectionView.Bounds.Size.Width / 129f);
layout.ItemWidth = 129f;
This will allow the layout to adjust drawing based off of a change, such as a flipped orientation. Notice that this is called in ViewDidAppear, which allows some recalculations to be done when the view appears, if necessary.
Finally, return to the AppDelegate class and pass in the layout, followed by the data. Compile and run the project, and you should have a functioning waterfall UICollectionView!
Reviewing the Steps
The process is quite simple to replicate.
1. Calculate each cell’s height.
2. Pass this height into the layout’s delegate.
3. Update your cell so that it matches the height in HeightForCell.
4. Configure your layout to match your specific needs.
5. Update the UICollectionViewController to deal with re-drawing, such as when the orientation changes.
Custom UICollectionViewLayouts are a great way to spruce up your boring collection views. Integration really isn’t that different from implementing a normal layout.
For further documentation, please visit the GitHub repository for the project. I plan on packaging this as a component for the Xamarin Component Store soon.
Also, a huge shoutout to Nelson for writing the original library, and Chris Hardy (@chrisntr) for helping me track down a garbage collection issue.
As a new writer, I always appreciate constructive feedback, positive or negative. This can be mailed to firstname.lastname@example.org. Additionally, if you have any other topics you would like to see covered, feel free to email me as well.