Building The Custom UIScrollView Menu From Digital Post

Digital Post, my newspaper app for the iPad, uses a number of custom user interface elements to build out the full user experience. One of these custom components is a horizontal topic selector that you can swipe and also tap to select individual topics.

Original Concept

Facebook for iPhone was one of the first apps to use a horizontal scroller to let you navigate various screens or topics. When first designing the interface for Digital Post I thought it was a perfect opportunity to do my own version suitable for an iPad app.

Building The Main Slider

The requirements were simple: can be swiped left or right and each item in the menu can be selected. This led me to make the main component a UIScrollView subclass. I subclassed it because I needed to do my own custom drawing in its drawRect method to execute the design. Let's take a look at the drawing code, it's very simple:

- (void)drawRect:(CGRect)rect {
    UIImage *bg = [[UIImage imageNamed:@"slider.png"]
      stretchableImageWithLeftCapWidth:15 topCapHeight:0];
    [bg drawInRect:self.bounds];
}

Here we're taking a PNG, stretching it horizontally, and drawing it in the precise location that this scrollview is located. The left cap of 15px means that the first 15px of the image are kept pixel-precise, the 16th pixel is used to stretch across the wide area, then the final right pixels are kept pixel precise also. This is a common technique to execute custom designs, I wish I could do this in CSS!

Adding The Tappable Topics

To make a UIScrollView actually scroll you need to know the total width (or height) of the content it contains. For my scrollview, I programmatically add the tappable topics and then calculate the total width of them once I'm finished. I first thought to make each topic a custom UIButton but for some reason, if the buttons are one-after-another with no pixels in between, the touch events they intercepted stopped the slider from scrolling. I couldn't quite figure out the issue but fortunately there are numerous ways to accomplish the same design. Instead of UIButtons I decided to use UILabel subclasses and add the tap events myself using UIGestureRecognizers, one of the new APIs available on the iPad. Here's the code that calculates the total width of this scrollview's content:

CGFloat contentWidth = 20;
for( NSString *string in topics ) {
    contentWidth += [string sizeWithFont:[UIFont boldSystemFontOfSize:18]].width + 30;
}
self.contentSize = CGSizeMake(contentWidth, self.bounds.size.height);

Here I'm using NSString's method sizeWithFont to calculate the exact size of a string rendered using a given font. The 30px extra is to account for 15px padding on the left and right of each one. Once I've iterated across my entire array of topics, I now have an exact pixel amount to assign to the contentSize property.

After calculating the total width, I add each topic one at a time to the scrollview. (DPTopicLabel is my UILabel subclass.)

CGPoint startingPoint = CGPointMake(10, 0);
for( NSString *string in topics ) {
    CGRect labelRect = CGRectMake(startingPoint.x, startingPoint.y,
      [string sizeWithFont:[UIFont boldSystemFontOfSize:18]].width + 30,
      self.bounds.size.height);
			
    DPTopicLabel *label = [[DPTopicLabel alloc] initWithFrame:labelRect andText:string];
    if( [string isEqualToString:@"Top Stories"] ) {
        [label setSelected:YES];
    }
			
    UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc]
      initWithTarget:self action:@selector(handleTap:)];
    [label addGestureRecognizer:tap];
    [tap release];
			
    startingPoint.x += label.bounds.size.width;
    [self addSubview:label];
    [label release];
}

First, I create the CGRect that will be the exact position of my tappable topic. The CGPoint startingPoint is updated at the end of each iteration to push it ahead to where the next topic will go. Next, I create my new DPTopicLabel and use my custom initWithFrame:andText: method to pass in what the text should be. If the string is "Top Stories" then I call my setSelected method which draws the custom background for a selected topic.

After I create the topic I need to make it respond to touch events. There are a few ways to do this but I like how the new gesture recognizers work so I used that. Once you add a gesture recognizer to a view, you tell it what type of gesture you want it to recognize (complicated, eh?) and then what method you want it to call when it happens. In this case I'm catching tap events and passing in my handleTap method which will toggle the selected state of my label.

All that's left to do at the end is change my startingPoint variable and add the label to the overall scrollview. Done!

Conclusion

This isn't magic and it's not all that complex. Building custom UI components is all about being thoughtful and planning out each step. The steps here:

  1. Add a scrollview
  2. Draw its background
  3. Add custom labels one at a time
  4. Set one of those to be selected, that has a custom background

Feel free to use these techniques & code in any personal or commercial projects you may work on.

Featured Project

Design Then Code