Using a static UITableView as a layout device

The problem

It wasn't long after I started iOS development that I realised that UITableView can be used not just as a way of displaying a list of items but also as a way of managing a vertical layout of almost any interface. Especially when the height of the interface is variable and needs to scroll but also grow or shrink depending on content.

iOS 8 has added to the usefulness of UITableView in this second scenario by having tableview cells automatically calculate their own height based on AutoLayout Constraints added to them.

Take the Mail app for instance. Mail is a perfect example of these two uses of UITableView. The "Mailbox" view shows a list of email items in your Inbox. While the "Mail" view shows a single email laid out with From, To, Subject, Date, Body, etc... The mail view also has to grow or shrink depending on content. The longer the email is the more the view has to scroll. Also if there is a longer list of recipients then the "To" section has to grow to display them.

Something that seems to happen is people begin laying out the view and then suddenly realise that it will need to scroll so they embed their views into a UIScrollView. This is a perfectly acceptable way of creating this layout with one caveat: once you have done this you can no longer use AutoLayout to layout anything inside of the scroll view. You either have to lay everything out manually... CGRectMake(x, y, width, height) or you can place everything inside another view and it all just gets really complicated so I'll go no further.

Anyway, I thought I'd create a little example to show how you can, very easily, use UITableView to manage the vertical layout of a growable/scrollable view.

The app is a very simple app that displays details about some random animals that I pulled from Wikipedia. I've put all the information into a Plist file called "Animals.plist" and added the images to the app bundle just to make it easier so we don't have to deal with any network connections.

The Preview

Here's what the final app looks like...

It will have a list of animals in a tableview.

Animal List View
Animal List View

When you tap on an animal it will show details of the animal.

European Badger Page Top
European Badger Page Top

Which is scrollable and grows to accommodate all the text.

Animal Detail Page scrolled to bottom
Animal Detail Page scrolled to bottom

Creating the list of animals

To begin with lets create the Animal class. This is going to be the objects that we display in the app.

#import <Foundation/Foundation.h>

@interface Animal : NSObject

@property (nonatomic, strong) NSString *authorName;
@property (nonatomic, strong) NSString *authorPhoto;
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *photo;
@property (nonatomic, strong) NSString *species;
@property (nonatomic, strong) NSString *text;

@end

As you can see it is very simple. There is no code in the .m file. All the properties are NSStrings. This means that to display any images we'll need to use [UIImage imageNamed:animal.photo].

Next lets delete the default UIViewController from the storyboard and add UINavigationController and UITableViewController. I've also created a prototype cell to display an animal.

Storyboard with UINavigationController and UITableViewController. Prototype cell created.
Storyboard with UINavigationController and UITableViewController. Prototype cell created.

I created a custom UITableViewCell subclass called AnimalTableViewCell this just contains IBOutlet properties for the UIImageView and the UILabel so we can populate them.

Next we need to program our AnimalListTableViewController. This is out UITableViewController subclass selected in the storyboard.

In the private category I create an array to store the animals.

@interface AnimalListTableViewController ()

@property (nonatomic, strong) NSArray *animals;

@end

And then we create the array inside viewDidLoad...

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    NSString *path = [[NSBundle mainBundle] pathForResource:@"Animals" ofType:@"plist"];
    
    NSArray *animalArray = [NSArray arrayWithContentsOfFile:path];
    NSMutableArray *tempArray = [NSMutableArray array];
    
    for (NSDictionary *dictionary in animalArray) {
        Animal *animal = [Animal new];
        [animal setValuesForKeysWithDictionary:dictionary];
        [tempArray addObject:animal];
    }
    
    self.animals = tempArray;
}

And now our UITableViewDataSource functions...

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    // Return the number of rows in the section.
    return [self.animals count];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"AnimalCell" forIndexPath:indexPath];
    
    // Configure the cell...
    [self configureAnimalCell:(AnimalTableViewCell *)cell withAnimal:self.animals[indexPath.row]];
    
    return cell;
}

// I do this by default as it's good practise when using NSFetchedResultsController.
// Plus it makes it easier to see what's happening.
- (void)configureAnimalCell:(AnimalTableViewCell *)cell withAnimal:(Animal *)animal
{
    cell.animalImageView.image = [UIImage imageNamed:animal.photo];
    cell.animalNameLabel.text = animal.name;
}

This is all we need to get the list of animals loading. Run the app to make sure you are seeing the list of animals. (I'll be adding the project to GitHub for you to have a look around it).

Creating the animal detail view in Interface Builder

Right, now that we have our list of animals we need to create a way to display a single Animal. Each Animal has a name, latinName, photo, text, authorName and authorPhoto. I'm going to lay this out so that the name and latinName are displayed at the top. Followed by the photo. Then the text and lastly the author photo and name.

In the storyboard I've added another UITableViewController for our Animal Detail view.

In our tableview I'm going to create it as follows.

Row 0 will contain the name and latin name.
Row 1 will contain the photo.
Row 2 will contain the text.
Row 3 will contain the author details.

To do this I first set table view content property to "Static Cells" with 1 section.

Set the Tableview Content to "Static Cells".
Set the Tableview Content to "Static Cells".

The difference between prototype and static cells is subtle but powerful.

A prototype cell doesn't actually exist when you create it in Interface Builder. Instead it creates a template that is used to define cells when they are dequeued at runtime. This means that you can't create IBOutlets from the UI elements inside the cells because they're not actually real.

A static cell does exist. Just like when you add a button to a standard view controller. You can create IBOutlets to the cells and to the UI elements inside the cells. Also, this means there is only one of each cell that you create. They are real "tangible" objects that you can reference directly in code.

I gave the first section 4 rows one for each bit of data I want to display. Then add the relevant elements to each row...

Added the UI Elements to each static cell.
Added the UI Elements to each static cell.

Now for the important part. We need to add constraints to the views so that they work with automatically sized cells.

Add constraints and make sure to cover the full height of the cell.
Add constraints and make sure to cover the full height of the cell.

What you need to make sure is to add constraints all the way from the top to the bottom of the cell. This is because iOS8 will use these constraints and, at runtime, it will determine what the height of the row should be based on its content. We also need to make sure the labels aren't compressed by setting the vertical content compression resistance to 1000.

Repeat this on the other rows. The most important one is the text cell. I used a UILabel and set the number of rows to 0. Then I added constraints between all edges of the cell. Again, this is so that the cell will be resized automatically without us having to do anything.

Animal text label with constraint to all edges of the cell.
Animal text label with constraint to all edges of the cell.

Creating the Animal detail view in code

I created a subclass of UITableViewController called AnimalDetailTableViewController. This is the subclass I set in the storyboard. Again, it is a very simple class.

In the .h file add an Animal property. This will be populated during the segue into this controller.

#import 

@class Animal;

@interface AnimalDetailTableViewController : UITableViewController

@property (nonatomic, strong) Animal *animal;

@end

Then connect up all the UI elements we added in the storyboard...

#import "AnimalDetailTableViewController.h"
#import "Animal.h"

@interface AnimalDetailTableViewController ()

@property (weak, nonatomic) IBOutlet UILabel *titleLabel;
@property (weak, nonatomic) IBOutlet UILabel *dateLabel;
@property (weak, nonatomic) IBOutlet UIImageView *articleImageView;
@property (weak, nonatomic) IBOutlet UILabel *articleTextView;
@property (weak, nonatomic) IBOutlet UIImageView *authorImageView;
@property (weak, nonatomic) IBOutlet UILabel *authorNameLabel;

@end

Then in viewDidLoad I populate these...

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.titleLabel.text = self.animal.name;
    self.dateLabel.text = self.animal.species;
    self.articleImageView.image = [UIImage imageNamed:self.animal.photo];
    self.articleTextView.text = self.animal.text;
    self.authorImageView.image = [UIImage imageNamed:self.animal.authorPhoto];
    self.authorNameLabel.text = self.animal.authorName;
}

Adding the segue

Next we can add a segue from the AnimalListTableViewController to the AnimalDetailTableViewController.

During this segue we populate the animal depending on which cell is tapped...

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    if ([segue.identifier isEqualToString:@"animalSegue"]) {
        AnimalDetailTableViewController *controller = segue.destinationViewController;
        controller.animal = self.animals[[self.tableView indexPathForCell:sender].row];
    }
}

If we run the app at this point we can see that the data is being populated and displayed but the cells aren't resizing properly...

The data is there but the cells are not being resized. You can see the text being truncated with an ellipsis.
The data is there but the cells are not being resized. You can see the text being truncated with an ellipsis.

The last thing we need to do is "enable" the auto resizing of the cells.

To do this we add the UITableViewDelegate method to the AnimalDetailTableViewController class...

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    return UITableViewAutomaticDimension;
}

Summary

That's it!

With very little code and no messing around with CGRectMake we've been able to make a decent looking interface layout that resizes itself based on the size of the content and scrolls properly.

It also works across all device sizes and the width automatically updates to fit the width of the device.

We could have done this with a ScrollView but it would have been a lot more difficult to manage properly.

With iOS7 you can still use this method it just requires a bit more calculation for the table view row heights because iOS7 doesn't size them automatically based on the AutoLayout constraints.

You can get hold of the project on github here...

I hope this helps you when you need to create a vertically scrolling layout in the future. Remember that UITableView isn't just for displaying a list of items but can be very useful for laying out a single item.

5 Comments

  1. Romain

    Thanks again for your article 🙂 Now I'm trying to implement a MKMapView in the DetailTableViewController. Can't you tell me how to do that please ?

  2. Hi Oliver,

    I like this article a lot, and I agree that static table views can be a great way to lay out an application's user interface. One of the key features provided by MarkupKit (the open-source project I mentioned in an earlier comment) is the ability to easily create static table views, either in markup or in code. This weekend, I took a stab at replicating your detail layout using MarkupKit, and I wanted to share the results with you in case you are interested:

    http://gkbrown.org/2015/11/01/using-a-static-uitableview-as-a-layout-device-markupkit-edition/

    Hope you find it useful!

    Greg

Leave a Reply

Your email address will not be published. Required fields are marked *