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.
Hi, wonderfull work, i try to implement to my app, but i don’t know where and how call when press the tab in my tabbar, please can you help me? my email address is spp_terry@hotmail.com, thanks
Well, it depends on where you’re starting the work that you want to cover with the spinner. If you look at something like the Youtube app, it displays the spinner whenever it requests XML from the server. It does that periodically when the view is displayed. Check out the callback methods on the UIViewController class, i.e. viewDidLoad, viewWillAppear, viewDidAppear.
Your dealloc method should just be written as follows. It is not necessary to check for nil, because sending a message to nil is perfectly acceptable and simply does nothing.
-(void)dealloc {
[container release];
[activityLabel release];
[activityIndicator release];
[super dealloc]
}
Superviews retain their subviews, so if you want you could autorelease your subviews at instantiation (as long as you add them to superview). Then you wouldn’t need to release at all, and your dealloc can be ditched.
That’s a good observation, John. If I remember right, the code this example was ripped from reused the spinner across more than one view, but in any case, I was definitely still learning the rules of the road at this point. Reminded me of the bad old days of addref/release in Windows COM programming :).
Bob. you’re quite correct. That was just my C++ trained programming brain refusing to allow me to do anything with a null pointer.
Hi, I’m just wondering why the ActivityOverlay class should be a UIViewController. Wouldn’t a UIView work just well? That way we can pretty much use that class in more places. Am I correct? I’m implementing this feature in my app tonight and wondering if I should be doing it this way :)
It probably will, Enrico. I’m still pretty much a beginner at iOS development, but I think the division of labor between the view and controller is a convention that you can choose to ignore if you like.
[Edit] To expand on this a bit. The controller class in my example is where I group together the three controls that make up the activity spinner: the UiView container, the label, and the spinner itself. It maintains the references to the controls and provides state management and methods to manipulate state. You could certainly choose to create a derived class from UIView, and store the state there. Basically this division of labor between controller and view seemed to mesh with the pattern followed in the examples I learned from. I don’t think it’s the only way to get things done.
Hi Mark,
Thanks for a great post. I had the exactly same problem. However, when i added my “loading view” to the superview with [self.superview.view insertSubview:blockerView.view aboveSubview:self.view]; i got another problem. The view got automatically resized to fit the entire tableview. That’s not what i wanted and i couldn’t figure out what was wrong and how to resize it.
Then i figured out the problem. Since the tableview is added inside a navigationview the solution to the problem was to simply add the loading view to the navigationview. The following did the job for me:
[self.navigationController.view addSubview:blockerView];
Then the loading view comes ON TOP of the tableview and not under it :)
Hi Mark, can I download the sample code somewhere? I want to understand how it works but I am confused!
Hi, Jens. Unfortunately I don’t have a complete sample available for this. The code was abstracted from a proprietary app I was working on. I’m actually not doing any active iOS dev at the moment due to a .NET/WPF project that needed me more :). The Mac and xCode have passed on to one of my colleagues. Still, I think all the important pieces are given in the post, so if you put together a small test app with a UITableView and play with it I expect you’ll see how it works quick enough.
Thanks for this. I didn’t do it exactly the same of course. But this was a great starting point.
In terms of correctness I wonder if it makes more sense (or is valid in an MVC sense) that the class instantiating the container view actually begin the animation or even have direct access to the activity view, colors, etc. This would give a lot more flexibility as to how the activity indicator looks.
Regardless, I would up doing nearly what you do but a little larger, with a transparent background, translucent background with the centred container, and rounded corners (similar to border-radius:8px in CSS).
– (void)loadView {
[super loadView];
container = [[UIView alloc] initWithFrame:CGRectMake(0, 5, 110, 75)];
label = [[UILabel alloc] init];
label.text = @”Loading”;
label.textColor = [UIColor whiteColor];
label.backgroundColor = [UIColor clearColor];
label.font = [UIFont boldSystemFontOfSize:14];
[container addSubview:label];
label.frame = CGRectMake(0, 50, 110, 25);
label.textAlignment = UITextAlignmentCenter;
activityIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
activityIndicator.backgroundColor = [UIColor clearColor];
[container addSubview:activityIndicator];
activityIndicator.frame = CGRectMake(30, 0, 50, 50);
[self.view addSubview:container];
container.center = CGPointMake(frame.size.width/2, frame.size.height/2);
container.backgroundColor = [UIColor colorWithRed:0.0f green:0.0f blue:0.0f alpha:0.5];
container.layer.cornerRadius = 8; // Need #import
self.view.backgroundColor = [UIColor clearColor];
}
One thing that I found strange but maybe this is normal. Even though I use addSubview in my class calling for the container, I have to manually call viewWillAppear and viewWillDisappear.
I just figured out a muuuuuch simpler way of doing this. UITableView has an @property UIView tableHeaderView which is an optional view that you can put “above” the cells of your UITableView. set this view to be your custom loading view with spinner and label and then set tableHeaderView to nil once you’ve finished loading your data. voila
Still works great in 2012. Thanks!