Brian’s Brain

I Need a Tagline

Developing a Custom Grid Control

BDGridView is the heart of the user experience of Pholio, and this article should give you the high-level overview you need to understand the source code (.h, [.m] (https://github.com/bdewey/Pholio/blob/master/Classes/BDGridView.m)). BDGridView a versatile class. I can use it to show all of the galleries as tiles, or I can show the individual photos in a set:

The Pholio grid view

The Pholio context menu

To achieve this versatility, BDGridView uses a similar delegate pattern to table views. Like a table view, BDGridView has a dataSource delegate that tells the grid view how many cells there are (gridViewCountOfCells:) and supplies the content of each cell (gridView:cellForIndex:). The only additional thing that the grid data source must do that a table data source does not is supply the size of each cell (gridViewSizeOfCell:).

With the delegate responsible for determining what cells to display, the job of BDGridView is simply to lay out the cells in a grid. Because there are likely more cells than will fit within the drawing area, BDGridView subclasses UIScrollView.

UIScrollView

UIScrollView lets you have content that is larger than what you can view on the screen. The scroll view’s contentArea property defines the logical size of your view — it’s the blue line in the diagram above. The contentOffset is the upper left corner of the visible area. If you change contentOffset, the visible area will move around. The standard bounds property is the full viewable area — the black line in the diagram above.

The trick to using a UIScrollView effectively is to only create the subviews you need to draw the visible area. For BDGridView, all of this work happens in layoutSubviews. Here’s the rough outline of what happens. (Note: Much of this comes from Apple’s excellent PhotoScroller example.)

  1. I recompute how large the contentArea is. In the simple case where every grid cell is the same size, this is trivial. I make the width of the contentArea match the width of the bounds. This means I will use all available horizontal space with no side-to-side scrolling. Once I’ve fixed the width of contentArea, I compute how many cells will fit across in one row. (The actual implementation isn’t simple division because I might want to guarantee a minimum distance between each cell in the row, but conceptually it’s just division.) Finally, once I know the number of cells per row, I can compute the number of rows I need to display all of my cells. The size of the cell determines the height of each row, and simple multiplication gives me the height of contentArea.

    Why do I do this each time in layoutSubviews? To handle rotation! When you rotate your iPad, the width of the viewable area changes… which could mean a different number of cells per row, and therefore a different number of rows and a completely different contentArea at the end of the rotation.

  2. Internally, BDGridView maintains a set of all of the cells that are currently visible, creatively named visibleViews. The next step in layoutSubviews is to walk through each cell in visibleViews and make sure it’s still supposed to be visible. Perhaps it scrolled off the screen? If it’s not on the screen any more, the cell gets removed as a subview, removed from visibleViews, and added to recycledViews.

  3. Next, layoutSubviews walks through each cell index that is supposed to be visible and makes sure that it really is visible. If it finds a cell that is supposed to be visible but isn’t, it asks the view delegate to create the cell for that index. Just like a table view delegate, the grid view delegate will reuse a cell from recycledViews if one is available.

  4. Finally, layoutSubviews goes through each visible view and makes sure that the frame property is set appropriately: This will make sure each cell is in the right place in the content area. Note this is done for all visible cells and not just the ones that were just added to the set of visible cells, again to handle rotation. When you rotate the screen, the position of currently visible cells may need to change to handle the new width of the content area.

One Wrinkle: “Drop Cap” Style

In Pholio version 2.2, I added support for an optional “drop cap,” named after the typographic practice of having the first letter in a paragraph span several lines of text. This one simple change lets me create layouts with much more visual interest than a monotonous grid. You can see in the following screenshots that the Nature tile spans two rows and two columns.

Grid index Cell index

To support drop cap cells, I formalized the concepts of a grid index and a cell index. The grid index defines the location of a cell in the content area, and the cell index defines the item in the grid. If you use a drop cap, the first cell index takes up more than one grid index. That’s a mouthful, so hopefully pictures help. In the screenshots above, the image on the left shows the grid indexes, and the image on the right shows the cell indexes.

Most of the work of supporting the drop cap style was writing the two routines that convert a grid index to a cell index, and vice versa. Armed with those routines, I had to make sure that whenever BDGridView deals with the location of things in the content area, it uses the grid index of the cell. (For instance, when setting the frame of a visible cell in layoutSubviews, I use the grid index of the cell.) However, whenever I ask the delegate for a new cell to display, I use the cell index.

This covers the basics of BDGridView. While this class is not currently written as a stand-alone library, I designed it to be general-purpose. I hope you find either the code or the ideas helpful in your own projects.