Monday, January 31, 2011

Easy custom UITableView drawing

It is really easy to customize your UITableViews. I'll show you how to completely customize the appearance of UITableViews without overriding or subclassing and without the need for any tricky hackery.

Make my table pretty

The core of most iPhone applications is the UITableView. To make your iPhone application stand out, the simplest way is to make your UITableView look good.

Customizing your UITableView can be really easy. You don't need custom drawing code. You don't need subclasses of anything. Cocoa Touch provides all the drawing capability you need, all you have to do is use the right classes in the right ways and provide the layout.

The sample application

The approach I'll show you will turn the table on the left into the table on the right:

customtableview.png

Left: a default UITableView with three rows. Right: the same table view after customization.

How to fail at UITableView customizing

Coming from Mac OS X made it harder for me — UITableView needs to be customized in a very particular way and structurally, it is very different to Mac OS X's NSTableView and NSCelldrawing.

The following are all really bad ways to customize a table (even though you can make it work):

  • Subclassing UITableView to customize the drawing of cells
  • Subclassing UITableViewCell to customize the drawing of cell content
  • Creating your own array of UITableViewCells and returning these instead of usingdequeueReusableCellWithIdentifier:

About the second point: it is okay to customize UITableViewCell — but you shouldn't really use it for drawing. The UITableViewCell class is more of a controller class — it handles behaviors and layout, not drawing. You can customize UITableViewCell to load a specific contentView (and do the custom drawing there).

That last point (that you should always use dequeueReusableCellWithIdentifier:) is only peripherally related to drawing but it will significantly slow your drawing down if you avoid the normal cell queuing architecture.

How to succeed at UITableView customizing

There are only a few points to understand related to table drawing.

First: the UITableView does not itself draw anything except the background. To customize the background of a UITableView, all you need to do is set its backgroundColor to [UIColor clearColor] and you can draw your own background in a view behind the UITableView.

Second: The tableHeaderView (and the table footer and section headers and footers) need not be just a title. You can insert your own view, with its own subviews in the table header, giving layout and custom drawing freedom.

Third: UITableViewCell is composed of 5 different subviews. Customizing the right subview is the secret to good UITableViewCell drawing. The subviews are:

  1. backgroundView — the entire background of the row (including what looks like theUITableView's background in UITableViewStyleGrouped style tables.
  2. selectedBackgroundView — replaces the backgroundView when the row is selected.
  3. image — a customizable image (not actually a subview) at the left of the cell.
  4. accessoryView — a customizable view at the right of the cell.
  5. contentView — a customizable view between the image and the accessoryView(technically, it extends behind the image).

You can customize any of these (except image which must be a UIImage) using your own custom drawn views.

However, since the pixel size of the table never changes, it is often easiest just to useUIImageViews for each of them. Then you can take highly complex views drawn in separate programs, cut them into the 5 necessary pieces and let the automatic caching of UIImage's named image cache manage your memory for you.

There is an argument against drawing your views in code and that is that the iPhone's drawing is not nearly as fast as Mac OS X. Operations like gradients and multiple overlapped components can really tax the iPhone.

Custom drawing code is a good choice for simple and flat colour drawing. In most other cases — as in this post — I recommend you use UIImageView to draw your views in a table.

Implementation

With all custom drawing handled by UIImageView, that still leaves some work to do. You must handle all layout and configuring of views.

Configuration of the UITableView and layout of the table header

As an example of what that means, have a look at the viewDidLoad method for this post:

- (void)viewDidLoad {     //     // Change the properties of the imageView and tableView (these could be set     // in interface builder instead).     //     tableView.separatorStyle = UITableViewCellSeparatorStyleNone;     tableView.rowHeight = 100;     tableView.backgroundColor = [UIColor clearColor];     imageView.image = [UIImage imageNamed:@"gradientBackground.png"];          //     // Create a header view. Wrap it in a container to allow us to position     // it better.     //     UIView *containerView =         [[[UIView alloc]             initWithFrame:CGRectMake(0, 0, 300, 60)]         autorelease];     UILabel *headerLabel =         [[[UILabel alloc]             initWithFrame:CGRectMake(10, 20, 300, 40)]         autorelease];     headerLabel.text = NSLocalizedString(@"Header for the table", @"");     headerLabel.textColor = [UIColor whiteColor];     headerLabel.shadowColor = [UIColor blackColor];     headerLabel.shadowOffset = CGSizeMake(0, 1);     headerLabel.font = [UIFont boldSystemFontOfSize:22];     headerLabel.backgroundColor = [UIColor clearColor];     [containerView addSubview:headerLabel];     self.tableView.tableHeaderView = containerView; }

This method handles the configuration of the tableView (setting the backgroundColor, rowHeightand sets an image behind the table) but also creates its own layout for the table header.

The layout of the header here is for the table's header view. You can include a custom header for every table section by implementing the UITableViewDelegate methodtableView:viewForHeaderInSection:. There are equivalent properties and methods for the table and section footers.

It is possible to handle this type of layout in Interface Builder and load the XIB files for this type of layout. Sadly though, on the iPhone, reading loading lots of views from XIB files is slow (I suspect this is due to slow reading from the Flash memory) and doesn't always allow configuration of every property.

For this reason, I normally sketch my views in Interface Builder and then manually recreate the same thing in code. That's what I've done here: picking coordinates for the headerLabel that looks balanced in the view.

Cell backgrounds

The cell background needs to incorporate the tops and bottoms of table "sections". For this reason, the backgroundView and selectedBackgroundView normally need to be set on a row-by-row basis.

In your tableView:cellForRowAtIndexPath: method where you are configuring the cell for a given row, this code will handle that behavior:

UIImage *rowBackground; UIImage *selectionBackground; NSInteger sectionRows = [aTableView numberOfRowsInSection:[indexPath section]]; NSInteger row = [indexPath row]; if (row == 0 && row == sectionRows - 1) {     rowBackground = [UIImage imageNamed:@"topAndBottomRow.png"];     selectionBackground = [UIImage imageNamed:@"topAndBottomRowSelected.png"]; } else if (row == 0) {     rowBackground = [UIImage imageNamed:@"topRow.png"];     selectionBackground = [UIImage imageNamed:@"topRowSelected.png"]; } else if (row == sectionRows - 1) {     rowBackground = [UIImage imageNamed:@"bottomRow.png"];     selectionBackground = [UIImage imageNamed:@"bottomRowSelected.png"]; } else {     rowBackground = [UIImage imageNamed:@"middleRow.png"];     selectionBackground = [UIImage imageNamed:@"middleRowSelected.png"]; } ((UIImageView *)cell.backgroundView).image = rowBackground; ((UIImageView *)cell.selectedBackgroundView).image = selectionBackground;
Layout within the contentView

Layout of elements within the contentView need only be set on construction of the contentView(not on a row-by-row basis).

Sadly, laying out UILabels in the contentView (like the "Cell at row X." and "Some other infomation." lables in this example) is a little verbose.

The following code is run immediately after the allocation of the UITableViewCell to position the "Cell at row X." label:

const CGFloat LABEL_HEIGHT = 20; UIImage *image = [UIImage imageNamed:@"imageA.png"];  // // Create the label for the top row of text // topLabel =     [[[UILabel alloc]         initWithFrame:             CGRectMake(                 image.size.width + 2.0 * cell.indentationWidth,                 0.5 * (aTableView.rowHeight - 2 * LABEL_HEIGHT),                 aTableView.bounds.size.width -                     image.size.width - 4.0 * cell.indentationWidth                         - indicatorImage.size.width,                 LABEL_HEIGHT)]     autorelease]; [cell.contentView addSubview:topLabel];  // // Configure the properties for the text that are the same on every row // topLabel.tag = TOP_LABEL_TAG; topLabel.backgroundColor = [UIColor clearColor]; topLabel.textColor = [UIColor colorWithRed:0.25 green:0.0 blue:0.0 alpha:1.0]; topLabel.highlightedTextColor = [UIColor colorWithRed:1.0 green:1.0 blue:0.9 alpha:1.0]; topLabel.font = [UIFont systemFontOfSize:[UIFont labelFontSize]];  // // Create a background image view. // cell.backgroundView = [[[UIImageView alloc] init] autorelease]; cell.selectedBackgroundView = [[[UIImageView alloc] init] autorelease];

In my mind, it seems like there should be a more efficient way to do this. I hold out the possibility that there is.

This code spends most of its time working out where the label should be placed. It needs to go right of the image, left of the accessoryView, middle of the row but above the "Some other information." label.

Other adornments

The accessoryView is just a UIImageView. The cell.image is set as a property. These are extremely simple additions but they make the table cells far more impactful.

Conclusion

You can download the EasyCustomTable project as a zip file (60kb).

iPhone Coding – Learning About UIWebViews by Creating a Web Browser

Today I will be showing you how to work with a UIWebview to create a basic web browser. Here is a screenshot of the app we are going to create.

screenshot_02

Create a View-Based Application

Ok, so let’s get started. Start by opening up Xcode and click File -> New Project. Next select View-Based Application and click Choose… Name this project something like iCodeBrowser and click Save.

screenshot_03

screenshot_04

Now we are ready to begin coding…

Create IBOutlets and IBActions

Before we create the interface for our web browser, we need to establish the IBOutles and Actions to interface with the UI elements in code. Start by opening up iCodeBrowserViewController.h and add the following code:

screenshot_06

Let’s take a look at this code line by line. The first thing we added was the to the end of our interface declaration. This is telling the app that this class will be the delegate for our UIWebview.

What does it mean to be the delegate you ask? Great question… A delegate is like a handler. It is responsible for implementing certain methods in order to handle events sent by the object they are the delegate for. So in our example, we are simply saying that we will implement some of the functionality of the UIWebView. This is needed so we can capture certain actions of the UIWebView such as a click on a link or so we can tell when a page has started/finished loading. If it’s still unclear, ask me clarifying questions in the comments section.

Next, we see our 3 lines of declaring IBOutlets. These are the UI elements that we will be interacting with. In case you didn’t know, the UIActivityIndicator is the little spinner/loading animation that you see on various apps when content is loading. We will be using this to show that a page is currently loading.

Following this code, there are 3 IBActions. IBActions are functions that get called in response to a user interaction with the application (such as tapping a button). For our basic browser, we are only offering 3 types of functionality. gotoAddress which will take a user to the address they type in the address bar and goBack/Forward should be pretty self explanatory.

Creating the User Interface

Now, let’s create the interface using Interface Builder. I am going to be showing you how to do this in the video below.

Implementing the IBActions

Now that we have our interface, let’s make the app function. We need to implement our methods. Open up iCodeBrowserViewController.m and add the following code.

screenshot_01

We need to synthesize our properties to allow us to interact with them. Synthesizing automatically creates “getter” and “setter” methods for our properties. Next, let’s implement the viewDidLoad method. This is where we will be loading our “homepage”. Add the following code to the viewDidLoad method.

screenshot_08

ADVERTISEMENT

The viewDidLoad method gets called automatically by our application whenever this view first loads. We can know for sure that it will get called, so we can put our initialization code here.

The first thing we see is the urlAddress string. This will be our “homepage”. You can change this to any address you wish to start with. Next, we build a URL object with our string. We need to do this so we can make a web request. Following this, we build our web request and load it into the webView. This will display the homepage inside of our webview. Finally, we set the text of the address bar to the homepage address. This part is more for aesthetics to let the user know what page they are on.

Next, we implement the method that we connected to the UITextField’s DidEndOnExit method gotoAddress. Add the following code:

screenshot_09

This is similar to the code we wrote in the viewDidLoad method, except for the fact that we are getting our URL string from the address bar. This method gets called when the user presses the “Go” button on thekeyboard. The last thing to note here is we call the [addressBar resignFirstResponder] method. This simply tells the app to hide the keyboard when this method gets called.

The implementation of our Back and Forward methods are pretty easy. Go ahead and add the following code.

screenshot_10

UIWebViews are pretty cool because of the functionality they offer us built right in to them. We simply call[webView goBAck] to go back and [webView goForward] to go forward. This greatly simplifies the interactions with the webview. If we were to code that functionality from scratch, we would have to create a stack of URLs and continually push and pop them off the stack to keep track of where we need to go. Thanks Apple for not making us implement this.

Finally, we need to implement the delegate methods for UIWebview. These methods allow us to write our own code to respond to actions by the UIWebview. The first methods we will implement are the webViewDidStartLoad and the webViewDidFinishLoad methods. We will use these to show and hide the activity indicator. Add the following code:

screenshot_11

So when the request is first made for a ULR (before the page starts loading) the webViewDidStartLoad method gets called automatically. We use this opportunity to start our activity indicator to let the user know the page is loading. If you don’t have something like this, it simply feels like the app is frozen when in fact, it’s just loading the page. Finally, the webViewDidFinishLoad method gets called when the page is fully loaded. After this, we can stop the indicator (and it automatically hides itself).

The very last thing we need to do is define what happens when a user clicks on a link. Add the following method:

screenshot_13

This method gets called automatically whenever the user clicks a link. This method can be very useful if you want to make a native iPhone application that integrates with a web app. You can use this method to trap the user’s clicks and have your application respond to web links that get clicked. In our case, we need it to do 2 things. The first is to set the text of the address bar to the URL of the link that was clicked on and to load that address into the webview.

One thing to make note of: We do a check to see if the URL scheme is “http”. This is to ensure that the user typed http before their URL. You can add an else statement here that auto prepends the http if the user did not add it. This would allow you to type in a url such as “icodeblog.com” rather than having to type “http://www.icodeblog.com”. I chose to omit it for this tutorial.

Remember, all of this added functionality of a UIWebView can only be gotten if you tell your class that it implements the UIWebViewDelegate protocol as we did in our .h file.

The app should be complete! Click on Build and Go to see this baby in action. Remember, you must put “http://” in front of your URL’s.

I hope you have enjoyed this tutorial. If you have any questions or comments, feel free to leave them in the comments section of this post. You can download the source here . Happy iCoding!