Friday, April 16, 2010
My love/hate relationship with the iPhone SDK
There is a lot to hate about the iPhone SDK. But there's also a lot to love.
My app iTravel goes out and gets Wikitravel pages, and then gets subpages in the background. (eg if you ask for "New York City", it'll also go out and get "Manhattan", "Upper West Side", etc.) It also plots listings (sights, restaurants, etc.) for all these pages on a map. I wanted the background thread that was loading this data to automatically a new page's listings to the map, if the viewer was using one; so they can go to "New York City", go to the map, and then watch Manhattan slowly get barnacled by map annotations, one neighbourhood at a time.
I thought this was going to be difficult. I couldn't have been more wrong. Here's the background-thread code, in its entirety:
I'd like to do this in Android, too...but a) their map implementation works a lot slower, b) I don't think there even is a method to get the currently active Activity.
Now, the "showAnnotations:" method within MapViewController is obviously trickier. For one thing, it's synchronized, lest the user try to filter a map just when a background thread is adding listings - not really a UI issue, since this usually takes all of 1-2 seconds:
What follows is one of the reasons I hate the SDK. Basically, I want to do some fairly basic set arithmetic to ensure that we remove undesired annotations from the map (but keep them in our local "annotations" set in case we need to add them again) and add new ones that are desired. Because the "-unionSet:" etc. methods on NSMutableSet don't work at all like you'd expect, though, I basically have to do that by hand. I'll skip over that messy part to the good stuff:
My app iTravel goes out and gets Wikitravel pages, and then gets subpages in the background. (eg if you ask for "New York City", it'll also go out and get "Manhattan", "Upper West Side", etc.) It also plots listings (sights, restaurants, etc.) for all these pages on a map. I wanted the background thread that was loading this data to automatically a new page's listings to the map, if the viewer was using one; so they can go to "New York City", go to the map, and then watch Manhattan slowly get barnacled by map annotations, one neighbourhood at a time.
I thought this was going to be difficult. I couldn't have been more wrong. Here's the background-thread code, in its entirety:
-(void) refreshMapIfActive {
UIApplication *app = [UIApplication sharedApplication];
iTravelRightAppDelegate *appDelegate = app.delegate;
UINavigationController *controller = [appDelegate navigationController];
NSArray *viewControllers = [controller viewControllers];
UIViewController *currentController = [viewControllers lastObject];
if ([currentController class] == [MapViewController class])
[currentController performSelectorOnMainThread:@selector(showAnnotations:) withObject:nil waitUntilDone:NO];
}
I'd like to do this in Android, too...but a) their map implementation works a lot slower, b) I don't think there even is a method to get the currently active Activity.
Now, the "showAnnotations:" method within MapViewController is obviously trickier. For one thing, it's synchronized, lest the user try to filter a map just when a background thread is adding listings - not really a UI issue, since this usually takes all of 1-2 seconds:
-(void)doShowAnnotations:(NSArray*)annotationsToShow {
//autorelease pool
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
@synchronized (self) {
@try {
NSArray *wikiPageMarkers = [WikiPage getPageMarkersWithin:mapView.region];
BOOL added=NO;
double pageLevel = [Settings getMapPageLevel];
BOOL abovePageLevel = mapView.region.span.longitudeDelta > pageLevel || mapView.region.span.latitudeDelta > pageLevel;
if (abovePageLevel && [mapView.annotations count] > [wikiPageMarkers count]) { //clear away low-level annotations
[mapView performSelectorOnMainThread:@selector(removeAnnotations:) withObject:[annotations allObjects] waitUntilDone:YES];
[annotations removeAllObjects];
}
else if (!abovePageLevel)
{
UITabBarItem *selected = [self.tabBar selectedItem];
if (selected!=nil) {
//first, remove all annotations that don't fit the selection
NSMutableArray *toRemove = [NSMutableArray arrayWithCapacity:[mapView.annotations count]];
for (NSObject *annotation in mapView.annotations) {
if ([annotation class] == [Listing class]) {
Listing *listing = (Listing*) annotation;
if (!(selected.tag==MY && [listing isInMyListings] || selected.tag==[listing.category intValue]))
[toRemove addObject:listing];
}
}
[mapView performSelectorOnMainThread:@selector(removeAnnotations:) withObject:toRemove waitUntilDone:YES];
}
if (annotationsToShow==nil) { //if we don't have a specific request, get all the listings within the map's region
NSNumber *category = selected==nil ? nil : [NSNumber numberWithInt:selected.tag];
annotationsToShow = [ListingManager getListingsWithin:mapView.region forCategory:category];
}
//the following is incredibly messy because NSSet and NSMutableSet are unusable for our purposes.
What follows is one of the reasons I hate the SDK. Basically, I want to do some fairly basic set arithmetic to ensure that we remove undesired annotations from the map (but keep them in our local "annotations" set in case we need to add them again) and add new ones that are desired. Because the "-unionSet:" etc. methods on NSMutableSet don't work at all like you'd expect, though, I basically have to do that by hand. I'll skip over that messy part to the good stuff:
NSMutableArray *thereNotRequested = [NSMutableArray arrayWithArray:mapView.annotations];
for (annotation in mapView.annotations) {
if ([requested objectForKey:[annotation title]] != nil)
[thereNotRequested removeObject:annotation];
}
[mapView performSelectorOnMainThread:@selector(removeAnnotations:) withObject:thereNotRequested waitUntilDone:YES];
if ([arrayToAdd count]>0) {
[mapView performSelectorOnMainThread:@selector(addAnnotations:) withObject:arrayToAdd waitUntilDone:YES];
added=YES;
}
if (!added)
[self performSelectorOnMainThread:@selector(activityDone) withObject:nil waitUntilDone:NO];
}
@catch(NSException *exception) {
[Util doLog:[NSString stringWithFormat:@"Warning: adding annotations to map view failed: %@", [exception reason]]];
[self performSelectorOnMainThread:@selector(activityDone) withObject:nil waitUntilDone:NO];
}
@finally {
[pool release];
}
}
Labels: currentController, iPhone, maps, MapView, NSMutableSet, Objective-C, performSelector:, selector, selectors, sets, synchronization, threading, UINavigationController
Comments:
<< Home
1) UIViewController *currentController = appDelegate.navigationController.topViewController;
2) if ([something class] == [Whatever class])
should be written as
if ([something isMemberOfClass:[Whatever class]])
or
if ([something isKindOfClass:[Whatever class]])
2) if ([something class] == [Whatever class])
should be written as
if ([something isMemberOfClass:[Whatever class]])
or
if ([something isKindOfClass:[Whatever class]])
Your blogs are really good and interesting. It is very great and informative. It also plots listings (sights, restaurants, etc.) for all these pages on a map. I wanted the background thread that was loading this data to automatically a new page's listings to the map, if the viewer was using one; Fairfax Traffic Lawyer, I got a lots of useful information in your blog. Keeps sharing more useful blogs..
I recently worked on a project where I had to manage complex background tasks while ensuring the UI updates smoothly. It was similar to the frustration of trying to pay to write my assignment when you want quick results but face unexpected challenges. It's all about finding the right balance between efficiency and performance.
Subscribe to Post Comments [Atom]
<< Home
Subscribe to Posts [Atom]
Post a Comment