More Glassy Scrolling with UITableView

A few days ago I wrote about how to help make your table views scroll more smoothly. The tips and tricks in there definitely work, but I’ve just added another layer of smoothness on top of that. It’s rather heavy-handed, but I thought I’d throw it out there anyway.

It starts much the same, use a single view for your cell and do all your own drawing. The ultimate solution presented here though is to cache the whole damned view contents. Revolutionary? No. Extreme? Yes. But it’s the smoothest I’ve been able to get it for my situation. I’m not yet 100% sure I’ll ship with this thing turned on, but I think it’s a valid approach for you to explore for your own project.

In my case I’m slowed down a bit by database fetches, text layout, and rounded images with shadows. By only following the techniques in my last blog post on this topic it’s actually not too bad. But with this addition it’s a lot better.

It should be noted this should really only be used as a last resort due to the increase in memory required.

In order to use this method, you need to be able to uniquely identify a cell by some sort of ID (in my case it’s an NSNumber).

- (void)drawRect:(CGRect)rect
{
    if ( !_item )
        return;

    UIImage* cached = [sImageCache objectForKey:_item.identifier];
    if ( cached == nil )
    {
        CGRect    bounds = self.bounds;

        UIGraphicsBeginImageContext(
             CGSizeMake( bounds.size.width, bounds.size.height ) );

        // DRAW CONTENT HERE

        cached = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();

        if ( sImageCache == nil )
             sImageCache = [[NSMutableDictionary alloc]
            initWithCapacity:1];

        [sImageCache setObject:cached forKey:_item.identifier];
    }

    [cached drawAtPoint:CGPointMake(0,0)];
}

This is actually more expensive to draw in two ways:

  1. It means you are drawing and then blitting, instead of just drawing.
  2. The table view caches the contents of cells itself, so technically we’re buffering yet again.

But the advantage is that we have the caches around for more than just the visible cells (which is all UITableView cares about). So when things scroll back into view, they’re absolutely smooth as silk.

This does mean that as views first load in they will be less smooth than the next time they roll into view. If the user typically only scrolls in one direction, you might not see a lot of benefit. So you could decide instead to pre-cache items ahead of where the user is. So perhaps you precache the first 20 items and after that let it cache as they are encountered.

When To Dump the Cache

Obviously, memory-wise this is not cheap. How much should you save? When should you flush it?

There are many ways to deal with this:

  1. Let the system tell you when to flush. If you get a didReceiveMemoryWarning call, just flush the image cache completely.
  2. Cap the cache at some number of items and flush it when it gets to that size.
  3. Cap the cache at some number of items and drop the oldest item when a new item needs to be added (Least Recently Used).

I’m actually trying out #1 believe it or not. I also flush the cache when the view has its viewWillDisappear method called. Why keep it around if you’re not actively flushing.

Another trick is to use a timed cache. Wait for the table view to stop scrolling (possibly by listening for the scrollViewDidEndScrollingAnimation delegate method to be called. When called, start a 10 second timer. If the view starts scrolling again, clear the timer. This way, after 10 seconds of real non-interaction, you’ll get the chance to flush the cache. No point holding them around.

A timed cache was what we used in Mac OS X for menus. One of the old stand-bys for measuring ‘performance’ was how fast the menus dropped down as you dragged back and forth in the menu bar. So we used the same basic method: on first appearance, cache the image, and from then on use the cache. And then we’d set a 20 second timer to flush the buffers so we didn’t waste memory. Since then I’ve used this technique a number of times.

Here’s a quick boilerplate for how you could use a timer:

- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
    // the user is scrolling/dragging, we WANT the cache, don't clear it!
    [self.timer invalidate];
    self.timer = nil;
}

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView
                  willDecelerate:(BOOL)decelerate;
{
     // if decelerating, wait until it stops first, else start the timer now.
    if ( !decelerate )
    {
        self.timer = [NSTimer scheduledTimerWithTimeInterval:10.0
                         target:self
                         selector:@selector(flushCaches:)
                         userInfo:nil
                         repeats:NO];
    }
}

- (void)scrollViewDidEndDecelerating:(UIScrollView*)scrollView
{
    // We know for sure we're done, start the timer.
    self.timer = [NSTimer scheduledTimerWithTimeInterval:10.0
                       target:self
                       selector:@selector(flushCaches:)
                       userInfo:nil repeats:NO];
}

- (void)flushCaches:(NSTimer*)timer
{
    // FINALLY. Flush it.
    [MyFancyCellView flushCaches];
    self.timer = nil;
}

Anyway, there you have it. Probably the most extreme method to get ultra-smooth scrolling while still using UITableView. Use it wisely.



Dealing With Memory Warnings in View Controllers

When the iPhone decides you are consuming too much memory (and I’m not sure what the deciding factors are), it calls your application and ultimately your view controllers’ didReceiveMemoryWarning method. In here you’re supposed to clean any caches, etc. But there’s a small wrinkle in how you need to handle this in a view controller.

Is My View Still There?

Basically, there’s a comment in the template code that says something like:

    // Releases the view if it doesn't have a superview 

This is on the line to [super didReceiveMemoryWarning];. Well, this news about your view being released is good, because it means that you can undo what you might have set up in viewDidLoad, your go-to place to know your view is there and ready for use. In fact, I always try to do the minimum in any init method and save it all for viewDidLoad.

The problem is that you don’t really know if it threw the view out. You can’t just test self.view as this will recreate the view! So I’ve been using a little trick to help me out:

- (void)didReceiveMemoryWarning
{
    UIView* superview = self.view.superview;

    // Releases the view if it doesn't have a superview
    [super didReceiveMemoryWarning];

    if ( superview == nil )
    {
        // OK. NOW we can assume view is gone.
     }

     // Destroy any caches regardless here
}

Essentially I see if the superview was nil before calling the super method. If superview was nil, the view really does go away (unless you have a reference to it of course, in which case things get much more complicated). If the view didn’t go away, you still might want to destroy any caches anyway, as shown above.

When we know the view is gone, I usually unregister any observers I might have installed, since they tend to operate on the view, which will just bring it back into being, which is bad. If the view is hidden, why bring it back into existence if no one can see it? Plus, aren’t we trying to use less memory? It should stay dormant until it’s brought back into the visible world.

Anyway, that’s what I’ve learned to do to ensure that my view is gone and I can tear down virtually everything, as if the controller just got initialized. It sure would be nice if we had a viewWillUnload method to catch this for realz instead of what amounts to a best guess.

Other Considerations

Even if the approach above worked like a charm, you might have other reasons to keep some data around. I had a situation where I had a view pushed onto a navigation controller that referenced the view underneath. Basically an iterator of the list below it, ala Mail. So I couldn’t throw away all my data structures like I otherwise might have when the view disappeared. So I ended up employing this:

// We can only safely purge the world if we are on top.
BOOL isOnTop = ([self.navigationController topViewController] == self);

And inside the if (superview == nil) condition I also checked isOnTop. If not, I can’t destroy everything.

Keep in mind that isOnTop can be true even the view was not visible. In a tab bar it might be the top controller on the non-selected tab, for example. In any case, if you’re on top you can safely dispose of all of your data structures, as you know there’s no controllers above you.

So there’s two ways I cope with memory warnings. As mentioned, they probably won’t solve 100% of the needs out there, but it might give you some ideas for you own applications.



Glassy Scrolling with UITableView

Now that the veil of secrecy has been lifted, I think it’s time to share at least one bit of information that I’ve gleaned whilst writing my new iPhone application: how to do fast tables.

NOTE: If you’re not already familiar with how UITableView works, the rest of this might just sound like jibberish. But I think there’s still valid life advice in here, so please read on!

In fact, one of the reasons I even started to write my app was to prove that I could write an app that does super-fast tables. And in my app (which, yes, is still not-to-be-named), I even do my own rich text layout for every cell. And every cell is a different height. Even I was skeptical that I’d be able to pull of fast tables, but it worked out. Phew!

How did I accomplish this? Well, by applying a lot of what I’ve learned over the years. And learning a bit about how UIView performs.

Use Views Sparingly

I’m not quite sure what’s going on in UIView. Maybe it’s that all views are backed by Core Animation layers, but drawing using a bunch of views is a sure-fire way to slow yourself down for straight on scrolling performance.

For simple table views (settings, or profiles, etc.) you probably can get away with using views with subviews. But we want smoothness as the user is scrolling through hundreds of items. In these cases, if you need to write a custom cell, you should use one view that draws everything directly.

Use Opaque Views

If you have a transparent cell, this can slow you down. If a view is marked opaque, the view system knows it doesn’t have to render the area behind it and blend your view onto it. This seems to make a marked difference at times. I just ran into this myself and switched to filling the view with the background color of the table and it is noticeably smoother now.

Note that this can work against you if you want to use standard cell highlighting, as your item won’t turn blue since the standard highlighting relies on transparency. But it does work great for instances where the items aren’t clickable. In my case, it was a message view modeled after the SMS application.

Be Lazy and Cache Often

Quite simply: don’t do anything until you don’t need to do it. And when you do need to do it, try to do it only once.

As I mentioned I render rich text. Sadly, there’s no built-in way to do this at present, so I have to lay the text out myself. This is not really cheap. So I have a layout object that lays itself out and then I keep that object around all pre-calculated. This is very much the way you’d use something like ATSUI or NSLayoutManager. You do the hard work once, then store it off for later. Then when I draw the cell, I just run along the lines of text and render them. I also calculate the rectangles of everything I will draw ahead of time. Then rendering is just a quick operation.

If you are drawing images, try to cache them at the size you’ll render them. If you are resizing them every time you draw them, you’re in for a world of hurt. In my situation, they weren’t too much bigger than my target size and they’re never bigger than a certain size, so I didn’t even bother. But if the images could be any size, it’d be really important to resize and cache them someplace.

Another thing I needed to do was cache the heights of the cells. If you have a table with variable-height cells, the table view will run through every item in the table and ask it how high it is before it renders anything. In my situation, with all the layout I do, that is pretty costly (it can actually take up to 2 seconds on the device). So I cache the heights. As new items come in and go out of my list, I only need to calc the new items, so after the first reload, all subsequent reloads are really fast since I already have the values.

Another cache I use is for the background for my cells for one of my ‘looks’. In this case, I use [UIImage stretchableImageWithLeftCapWidth:topCapHeight:] to give me an image I can stretch across the background. BTW, this is one of the most useful image functions in there. It basically takes an image and separates into a piece that acts as an end cap and a stretchable part. There’s effectively a one-pixel band that gets repeated as it stretches. But I digress…

What I ended up doing was taking these images and, because my cells could only be certain heights, as I knew I was using one font size and the cells would be based on the number of lines, there were only a finite number of heights I’d need (I believe 5). So as I encountered the heights, I created a cached version of the stretched image using code like:

UIImage* background = [[self balloonImage] stretchableImageWithLeftCapWidth:0
                                           topCapHeight:35];
UIGraphicsBeginImageContext( CGSizeMake( rect.size.width, rect.size.height ) );

rect.origin.x = rect.origin.y = 0;
[background drawInRect:rect];

image = UIGraphicsGetImageFromCurrentImageContext();

UIGraphicsEndImageContext(); 

And then I just take that image and store it in the cache. Very simple. I also stored that as the class level so all views can use the cache.

Nothing’s ever free, of course, so keep in mind that if you cache too much, you might find yourself running out of memory. Be prepared to listen to memory warnings via the didReceiveMemoryWarning method and dump your cache if needed.

And finally, also keep in mind that the table view only keeps the visible cells around. Every time a cell comes into view, it asks you for a new cell. You can reuse cells, but even that only will get you so much. So don’t assume that once your list has completely been scrolled through that everything is loaded. You cannot rely on such behavior. This piece of information caused me to change my strategy a bit until I arrived at what I am describing here.

How Do You Know What To Cache?

Fortunately, I’ve done this for a while, and I know a lot of the gotchas. But no matter how much you know or think you know, the best way to get real answers is to use a tool like Instruments. It is indispensable for finding bottlenecks in your code. For my app, I just kept sampling my application and finding things I should work on. If you aren’t using sampler or something similar, you really should.

Again: indispensable.

What’s Next

Next time I might cover performance in general. How to make your applications fast and not impede the user-experience. I’ll also talk about how to handle low-memory. This can be a bit tricky at times.

Until then!

UPDATE: Since writing this I’ve sped things up a little bit by going one step further.



CoreGraphics is Right After All

A while back I wrote about a potential issue with compositing using Quartz. I’m happy to finally conclude that there in fact is no issue. Yes, Quartz does behave differently than Cairo, but I believe Quartz has the more consistent behavior throughout all its modes. I came to this realization after talking to the engineer who does the compositing work in Quartz.

The main difference is that Quartz will only ever touch pixels within the ‘clip’ of an object, whereas it seems that Cairo will touch pixels outside of it. After thinking about it for a bit this makes much more sense, and seems to be a much simpler thing to implement as well, so I’m not sure why Cairo seems to take the tack they do.

So for example, if you fill a circle in Quartz, only the pixels inside the realm of influence (the circular path itself) will have the Porter-Duff blending applied. This is why things look different in Safari and Firefox when using these modes in a canvas object.

I reported in my prior post that I thought that it should affect pixels outside the circle, but this no longer makes sense to me after talking with that engineer. Now, if you were to blit an image of the circle, the area of influence would be the image rect (intersected with any clip, obviously). Then you’d get results more like what you’d expect. So basically, once you know that fact, you can likely still do most of the things you’d want to do using these blend modes.

So in the end it just seems like limiting the blending to the effective clip area is the better method. It’s far simpler to implement, since much like painting the circle in say, source over, you only affect pixels inside the circle. Ever. And that is a constant through all composition modes. This makes it consistent and predictable.

So in the end, it’s right, as far as I’m concerned (and Apple, of course)!



Confessions of a Carbon Guy

Recently, I started thinking about writing some small application on Mac OS X. The app will never probably be written, but it did get me thinking about how I would write it these days. I figured if I were really writing something from scratch, I should probably do it in Cocoa. Being Mr. Carbon, this was quite a conceit. And if I was going to write it in Cocoa, I should really probably learn what makes Cocoa tick so I could write an app “the right way”. So I got a new book (my old one was way out of date) and started reading and messing around. (more…)



Could CoreGraphics Be Wrong?

I’ve always held CoreGraphics (Quartz) on Mac OS X in the highest regard. It’s an excellent drawing API. But a while back when I did the canvas implementation for Konfabulator, I noticed what I believe to be an error in their compositing modes. This, along with gdiplus lacking some necessary features, forced me to move to Cairo for our Canvas object.

The bottom line is that if you compare the CG compositing modes (currently only exposed via Safari’s canvas) with those of Cairo you’ll notice some differences. Compare this link on Safari and then on Firefox. There is a known issue with Cairo’s ‘darken’ mode. Ignore that one for now.

Instead, let’s focus on one in particular: destination-atop.

Destination-Atop

Note that on Safari it yields the same result as destination-over. As far as I can tell, this is wrong. When the circle is drawn, all pixels outside of the circles influence should be cleared. This is what cairo does. Otherwise, what’s the point of the different mode?

This all tells me that Quartz is wrong, which to be honest shocks the hell out of me. If I’m wrong, please let me know and link me to some corroborating evidence.

Update: I’ve since learned new information and decided Quartz is right after all.



Carbon: The Undead

I just downloaded the Eclipse SDK to check out a plugin for it, and I noticed it was done in Carbon. That got me thinking about the state of Carbon these days. Last year at WWDC, it was made very clear that Carbon is in a holding pattern now. Maintenance mode.

I can understand why Apple might do this, Cocoa is a full-featured application framework, and the objective-C runtime does allow you do do some pretty interesting things with it. Why have two frameworks when you can focus on one. Also, now that Carbon did its job and got the developers over to X, it can be put out to pasture.

Sadly, I think this is a bad move overall. And not because I put so much hard work into it, but simply because I think it allows you to do some things easier than Cocoa. Arguably more important things.

(more…)



Did I Mention XCode Sucks?

OK, so tonight I’m trying to get some work done. I’ve been annoyed that command double-click doesn’t work, so I figure out code sense isn’t turned on. I turn it on… crash. OK. Restart the app, open the project… crash. Third time’s a charm tho, so I open it again… crash. This is single-handedly the buggiest dev tool I’ve ever used. There was another bug I’m reminded of where I was showing someone how to set command line arguments and I had added a blank argument. Well, every time you went to run the target program, XCode would crash. Surprise! We didn’t realize it was that until I remember we played in there and I turned off the argument (it was checked). Worked fine.

XCode, I hate you. From hell’s heart, I spit at thee!

And now I have to now spend time figuring out what the f*#$ is going on with this piece of crap now. Yay! Time-saver!

UPDATE: The problem I had was related to indexing the project. Recently I tried using indexing again and it all finally seems to be working. Yay! But I still hate it :-)



Custom Window Frame in Carbon

I wrote this example ages ago, but it never saw the light of day. Today I decided to clean it up a bit and put it out there for others to play with. It’s a simple piece of code that shows how simple it is to put up a window with a custom frame that replaces the standard frame you’d see in a Mac OS X Window.

This is written in Carbon, not Cocoa. It’s fairly straightforward in Cocoa as well. I think the same basic principles apply, only there’s more of @ and[] and sometimes $#%!#(.

It uses the HIFramework stuff that’s part of the Apple Developer examples.

Enjoy.

CustomFrame.zip



I hate xcode
Update: since I wrote this, I’ve changed my tune completely. With recent releases, Xcode has now become about the best IDE I’ve used. I actually want to delete this post, and maybe I will someday. It was written in a fit of annoyance, and in hindsight, it was over the top.

Simple as that. I hate it. It sucks. The sad part is they really think they have something there. But the only thing they have is a worthless lump of crud. The other day it decided to thrash my system for no apparent reason. Same sources as always, etc. but it took about 10 minutes of thrashing before it stopped. My attempts to watch what was going on in ‘top’ saw memory down to 16MB (I have 2G). After it subsided (and I had tried killing that damn thing and starting over, but it did it again), I was greeted with 1.8 gig of memory again. That’s right, to build my (somewhat small) project, it pretty much ate every living resource on the planet. (more…)