I’ve only been developing for iOS for a couple of weeks now, but it’s already obvious how ubiquitous the UITableView is in user interfaces. This makes sense. iPhone/Pad/Pod apps are often of the “list info and drill” variety, and the UITableView brings a lot of utility to the, um, table for lists, and even for constructing static dialogs. UITableViews automatically scroll if your content is too long for the vertical screen space, and they give you a lot of control over how individual cells and rows are styled. In the app I recently worked on we used a UITableView to list some stuff that was fetched as XML from a REST webservice. The webservice was taking about 4-5 seconds to retrieve the data over a wifi connection, and I knew it would likely be slower over 3G data connections. To let the user know what was going on I wanted to display an animated activity spinner while the transaction was enroute. If you’ve used the YouTube app for iOS then you’ve seen one of these in action.
The spinner is displayed over the contents of the table view, without obstructing the tab bar control. You can freely switch to another tab while the spinner is displayed over the current tab. It’s a nice, clean way to show that one tab is busy, while allowing the other tabs in the application to remain available. When I tried to replicate this behavior in our app, however, I quickly ran into some quirks. UITableView might be a useful control, but it isn’t all that lenient about how its views are managed. More on that below. First, let’s make the spinner itself.
Fortunately the iOS UIKit contains a view called UIActivityIndicatorView that makes this a pretty easy task. Basically the indicator view provides the little animated spinner. You set up its properties and provide a container. Here’s what the declaration of a reusable spinner overlay might look like. The syntax highlighter doesn’t deal with Objective-C, so I am just using the C filter. Apologies if it doesn’t look right.
// MyActivityOverlayViewController.h #import <UIKit/UIKit.h> @interface MyActivityOverlayController : UIViewController { UILabel *activityLabel; UIActivityIndicatorView *activityIndicator; UIView *container; CGRect frame; } -(id)initWithFrame:(CGRect) theFrame;
As you can see, not much to it. We have an interface derived from UIViewController, with ivars to hold pointers to a label, the spinner, and a container to hold them both. There is also an init method that takes a frame. I’ll talk more about this below, but essentially I found it useful to establish the size I wanted the view to be at initialization. That gets stored in the frame ivar.
So that’s the controller interface. Next let’s look at some key aspects of the implementation. First the init method:
// MyActivityOverlayViewController.m #import "MyActivityOverlayViewController.h" -(id)initWithFrame:(CGRect)theFrame { if (self = [super init]) { frame = theFrame; self.view.frame = theFrame; } return self; }
This is a pretty simple init method. First it saves the frame, because it will be used later to size and center the nested controls, and then it sets the size of the default view created by the UIViewController. Next we need to create the controls and build our simple view hierarchy. The overridden loadView method is where that gets done:
-(void)loadView { [super loadView]; container = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 110, 30)]; activityLabel = [[UILabel alloc] init]; activityLabel.text = NSLocalizedString(@"Loading", @"string1"); activityLabel.textColor = [UIColor lightGrayColor]; activityLabel.font = [UIFont boldSystemFontOfSize:17]; [container addSubview:activityLabel]; activityLabel.frame = CGRectMake(0, 3, 70, 25); activityIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; [container addSubview:activityIndicator]; activityIndicator.frame = CGRectMake(80, 0, 30, 30); [self.view addSubview:container]; container.center = CGPointMake(frame.size.width/2, frame.size.height/2); self.view.backgroundColor = [UIColor whiteColor]; }
This method does a bit more work. After calling the superclass loadView it creates a container to hold the label and activity spinner. Having them inside a container makes it easy to center the group in the owning view. It then creates the label, sets a few style properties, and adds it to the container’s view as a subview. You can set these properties any way you want. I was shooting for essentially the same look as the YouTube app. The last thing that happens to the label is that its frame is set. The size of the rectangle used was figured out based on the font size and relative positioning I wanted, but what’s important is that the size was set after the label was added to its owning view (i.e. the container). I’m not sure why this was important, but it was. Hey, I’ve only been at this a couple of weeks.
The next thing that happens is that the activity indicator itself is created. You’ll note that I’m not autoreleasing any of these objects, so I have to make sure to release them in the dealloc override. I know that UITableView retains its added views, and if that is also true of UIView I could autorelease the controls here and let the view manage them, but as I wasn’t sure of the behavior I opted to play it safe. After the indicator is created it is added to the container view and its frame is also set. Lastly the container itself is added to the UIViewController’s view, it’s center is set to the center of the frame we saved in the init method, and its background color is set to white.
Just a few additional things to finish off the implementation. First we need to start the indicator spinning. The indicator view provides methods called startAnimating and stopAnimating for just this purpose, and you could provide pass-through methods on your overlay controller. That would be the way to go if, for example, you wanted to display the overlay and then start and stop the animation and alter the label as different actions occur. In my case I’m going to have it start spinning whenever its displayed, and stop when it goes away. For that purpose you can override viewWillAppear, and viewWillDisappear:
-(void)viewWillAppear:(BOOL) animated { [super viewWillAppear:animated]; [activityIndicator startAnimating]; } -(void)viewWillDisappear:(BOOL) animated { [super viewWillDisappear:animated]; [activityIndicator stopAnimating]; }
The only other thing we need to do is clean up after ourselves:
-(void)dealloc { if (container != nil) [container release]; if (activityLabel != nil) [activityLabel release]; if (activityIndicator != nil) [activityIndicator release]; [super dealloc] }
And that does it for a simple implementation of a UIView that can be initialized with a frame size, and that will create a centered combination of a light gray activity spinner and light gray label on a white background. Now all we have to do is display it somewhere. That, as it turns out, is the tricky bit.
To get something on the screen we have a couple of choices. We could tell the tab bar controller to present it as a modal, or to push it as a page. Neither is a very good choice in this instance. Pushed controllers are not modal; they come with a back button by default. Modal controllers take up the whole screen and prevent access to the tab bar. So that’s not what we’re after. The other alternative is to play with views. To do this we would create an instance of the ActivityOverlayViewController, and then pass its view to the addSubView method of our UITableView’s view.
None of my attempts at doing this worked. I tried different combinations of methods to insert the view and then bring it to the top, but whatever I did things got screwed up one way or another. Toward the end I actually got it to display, but the table view drew the cell border lines over it, and that I was not able to work around. Instead, what I had to do was insert the activity spinner view as a subview in the view that owned the table view. In other words, it had to be a sibling view of the table view, not a child, and it had to be on top. Here’s what I came up with:
-(void)showActivityView { if (overlayController == nil) { overlayController = [[ActivityOverlayViewController alloc] initWithFrame:self.view.superview.bounds]; } [self.view.superview insertSubview:overlayController.view aboveSubview:self.view]; }
So the first thing to note here is that my activity controller is initialized with a rectangle representing the bounds, or inside dimension, of my table view’s superview. The second thing is that it is explicitly added to that view using the method that let’s you specify a view over which it should be placed in the view stack. In this case I want it inserted above the table view.
And that pretty much does it. Add that method, and an ivar to hold the pointer to the ActivityOverlayViewController, to the controller for your table view and you’re all set. To get rid of it when your background action is done, call:
-(void)hideActivityView { [overlayController.view removeFromSuperView]; }
And of course remember to clean up after yourself in your dealloc method. Here’s what the result looks like…
The final caveat is that as I mentioned above I am quite new to all this, so if somebody knows a better way to get this done then I hope to hear of it. Meanwhile this works and achieves the desired behavior.