Making Grids Of Buttons in iOS

From time to time, we want to have a grid of buttons or other controls in our apps. Usually, they are to support some kind of keypad-like interface. Sure, you could use the keyboard itself, but that doesn’t always gel with the UI you are trying to present, or the workflow you want to support. You can try to use the Auto Layout features now in iOS. But I found there’s a simpler, and less frustrating, way to do this, using the UICollectionView.

What Is It?

The UICollectionView is like a more sophisticated UITableView. Rather than having a scrollable column of rows, it gives you the ability to have rows and columns. It adds a second dimension to tables. You can have it scroll horizontally or vertically, and it allows for cells of varying sizes, along with sections to break up groups of items.

Almost all the examples around UICollectionView show it being used to show grids of photographs, or books, or recipes, or other collections of “things”. For those sorts of uses, it is truly well-suited. But there are more sophisticated things you can do use it to your advantage.

The Concept

The goal is to build a grid of “buttons”, presumably for some kind of number pad. We want the number pad buttons to maintain a certain aspect ratio, and we want the grid centred in its space in the interface. All of the buttons must be visible (no scrolling to get to other buttons). The grid should also adapt to different screen sizes (altering the size of the buttons, but maintaining the same grid layout) and handle re-sizing itself when the screen is rotated.

You can probably do this using the Auto Layout features in iOS. I managed to make it work once (a year ago), and then never got it to work again. Frankly, Auto Layout is a pain for anything but the most trivial designs. The one thing I miss from OSF/Motif are the layout containers they had. It had a rich selection that made making a decent UI fairly easy.

Okay, so we don’t have the nice grid layout containers in iOS like Motif had. But we have something that’s close, and that’s the UICollectionView. It is very, very good at presenting a grid of “things”. We just have to coerce it a bit to do what we want.

Dynamically Calculating Sizes

To make this work, you will need to make your view controller a delegate for the UICollectionView and implement the methods associated with calculating the sizes and offsets. The specific bits to override will be:

  • collectionView:layout:sizeForItemAtIndexPath
  • collectionView:layout:insetForSectionAtIndex
  • collectionView:layout:minimumInteritemSpacingForSectionAtIndex
  • collectionView:layout:minimumLineSpacingForSectionAtIndex

An example (including a method to calculate the actual button size) is presented below. Here is a link to a sample project that shows this in action.

- (CGFloat)maxItemSize
{
    CGRect viewRect = self.collectionView.frame;
    CGFloat itemMaxHeight = (viewRect.size.height - ((kRows + 1) * kItemGap)) / kRows;
    CGFloat itemMaxWidth = (viewRect.size.width - ((kColumns + 1) * kItemGap)) / kColumns;

    CGFloat itemMaxDimension = itemMaxHeight;
    if (itemMaxWidth < itemMaxDimension)
        itemMaxDimension = itemMaxWidth;

    return itemMaxDimension;
}
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
    CGSize result;

    result.width = [self maxItemSize];
    result.height = [self maxItemSize];

    return result;
}
- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section
{
    UIEdgeInsets insets;

    // We want to center the grid vertically and horizontally

    CGRect viewRect = self.collectionView.frame;
    CGFloat itemMaxSize = [self maxItemSize];
    CGFloat totalGridHeight = (kRows * itemMaxSize) + ((kRows + 1) * kItemGap);
    CGFloat totalGridWidth = (kColumns * itemMaxSize) + ((kColumns + 1) * kItemGap);

    CGFloat horizontalInset = kItemGap;
    if (totalGridWidth < viewRect.size.width)
        horizontalInset = (viewRect.size.width - totalGridWidth) / 2.0;

    CGFloat verticalInset = kItemGap;
    if (totalGridHeight < viewRect.size.height)
        verticalInset = (viewRect.size.height - totalGridHeight) / 2.0;

    insets.bottom = verticalInset;
    insets.top = verticalInset;
    insets.left = horizontalInset;
    insets.right = horizontalInset;

    return insets;
}
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section
{
    return kItemGap;
}
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section
{
    return kItemGap;
}

We also want to handle when the device is rotated, and force the collection to reload.

- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
    NSLog(@"Size/orientation change");
    [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
    [coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
        NSLog(@"Collection is %f x %f", self.collectionView.frame.size.width, self.collectionView.frame.size.height);
        [self.collectionView reloadData];
    } completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
        NSLog(@"Completion: collection is %f x %f", self.collectionView.frame.size.width, self.collectionView.frame.size.height);
    }];
}

This forces the view to perform a reload, which will cause it to re-do the layout.

The Result

What we get is an app that presents a panel of buttons that change size with the device screen. When it first starts, you’ll see something like this:

If you rotate the device, it looks like this.

Notice that the buttons change size, and the whole thing stays centred in the screen.

You’ll want to do something more sophisticated to show the buttons highlighting, but It think this shows the basics. Hopefully this provides some new ideas on how to use the UICollectionView.

Advertisements