views:

592

answers:

3

I've set up an Objective-C category for an iPhone app's UIImageView class. The category's mission is to help load URL-based images asynchronously with memory/disk caching.

Now, in UIImageView+Cache.m I have access to an NSOperationQueue so I can kick off a loading thread. I create an NSOperation-derived object, initialized with the image URL and the target UIImageView, and a selector to perform on the target once the operation is complete. In the selector method, we set our freshly-loaded image (or, if not found, we set an alternate placeholder image), and we're done!

This works fine, until a UIImageView happens to be removed before the NSOperation completes. For instance, I have a previous/next segmented control in my UI that causes these UIImageViews to be removed and added anew (they're part of a larger "item" that is being viewed in the app), so it's very easy to tap these in rapid succession.

So if you decide to start tapping away before all the images are loaded - KABLAM! Unhappy thread has an invalid object and doesn't know it. :(

The closest thing I can find to help mitigate this is NSOperation's cancel and isCancelled methods, except you can't keep track of which operation object to cancel within a Category, because - if I understand correctly - Categories can't add IVARs to objects!

Maybe that means Categories aren't a good idea here? (Whines: "But I liiiiike Categories! Waaah!")

Advisement appreciated!

A: 

I would say there is no harm in moving this into your own UIImageView subclass. Sure, you may like categories - but if they don't do the job then why hesitate in moving to a design that does?

teabot
I don't mean to imply that I would hesitate. I'm just wondering if it's a good idea in general, design-pattern wise.
Joe D'Andrea
To your point about using a subclass, here's one example I just found! Haven't tried it yet, but wanted to share for those playing along at home:http://www.markj.net/iphone-asynchronous-table-image/
Joe D'Andrea
+1  A: 

I probably wouldn't use a category for this situation. Categories are useful, but are usually unnecessary. I'd only use a category if you have a really good reason to. What exactly are you putting in the category?

I think you could implement the whole thing in the NSOperation subclass, which would be the best solution. Put a retain on the image view so it doesn't get deallocated before the image is downloaded, and cancel the download if the view is not visible anymore. If that's not possible, then subclass UIImageView instead of using a category.

Tom Dalling
The category adds two methods to UIImageView: loadWithCachedImageForURL:(NSURL *)url and didFinishLoadingImageWithResult:(NSDictionary *)result. There is also an NSOperation subclass named ImageLoadingOperation, which is used by loadWithCachedImageForURL when kicking off the op. (The queue is defined elsewhere in a singleton class used for a handful of common objects.)Meanwhile, the retain idea sounds like it might work! I didn't think of that one. I'll report back - thanks!
Joe D'Andrea
Using retain helped! Thank you!
Joe D'Andrea
Well, retain/release. ;)As for checking visibility, the operation doesn't know what the target is (UIImageView, etc.). Now, I suppose I _could_ check to see what kind of an object it is, but the operation was originally devised to work with any object, not just image views. For the time being, I'm going ahead and loading the image regardless, and doing a retain in init, and a release in dealloc (for the NSOperation). Make sense?
Joe D'Andrea
Sounds good. The only drawback is that you may be downloading images that will never be displayed. If the user is flicking through a lot, it might become a problem. Maybe you could cancel based on an optional method, such as:if([targetObject respondsToSelector:@selector(hasBeenInvalidated)]){ if([targetObject hasBeenInvalidated]){ [self cancel]; }}
Tom Dalling
Getting back to the first part of your answer, I'm still using a category, which in turn uses an NSOperation behind the scenes. The idea was/is to endow a UIImageView with an additional method that loads/caches an image asynchronously, so NSOperation is used in one place vs. everywhere I have a UIImageView. Perhaps this is still ill-advised though? If so, how might I do this cleanly using _only_ NSOperation, while keeping all of my UIImageView instances nice and tidy elsewhere?
Joe D'Andrea
Whoops, I replied while you were replying. Exactly - that's precisely the drawback. The trick now is figuring out how to set up that selector. (The UIImageViews are often removed as a consequence of removing a superview that contains them, such as a UIScrollView.) Basically, I'm trying to make it self-contained so it's a set-it-and-forget-it type of operation. Perhaps what I do, then, is make the operation require something of type UIView and not merely an id. Then I know what I can check, property-wise. :)
Joe D'Andrea
Yeah you can make the NSOperation take an NSView, and cancel if [view superview] is nil. As for getting rid of the category, you could make a function on ImageLoadingOperation like +[ImageLoadingOperation loadImageFromUrl:(NSURL*)url intoView:(NSView*)view usingSelector:(SEL)selectorToCallWithResult]; It would create and fire off an ImageLoadingOperation with the arguments specified.
Tom Dalling
Hmm! I like the sound of that. So then I would add a class method to my ImageLoadingOperation class that instantiates _itself_ as a new operation, which then goes through the usual init/main/dealloc routine. I could move the caching into there as well.
Joe D'Andrea
One thing I liked about using categories is it made it easy to switch between cached and non-cached loads. The code looks almost identical. Just the method changes. Still, this is an interesting option, and it does eliminate one pair of source files.
Joe D'Andrea
Oh! Wait. We still need something to handle the selector when the load is finished. But now that selector is not encapsulated within a category. So it does eliminate the Category, but I need to handle that selector everywhere I use this op. OK then. Decisions, decisions. :)
Joe D'Andrea
A: 

Are you retaining the UIImageView by the NSOperation? Otherwise the imageView might be freed before the NSOperation completes, leading to kablooi central. You should do a retain and then, once you've done the setImage, do a release.

AlBlue
Yes indeedy! The image view is being retained in the NSOperation's init (assigned to self.view, where view is a nonatomic/retain property).Interesting point to make here (which you/I already know, but casual readers new to Objective-C may not): view = theView is not the same as self.view = theView when there are synthesized properties involved. The latter invokes the setter method (and all the benefits it provides). The former assigns to the ivar directly - no retain!For now though, I'm using a subclass of UIImageView that invokes NSURLRequest's requestWithURL:cachePolicy:timeoutInterval:.
Joe D'Andrea