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:


-(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: , , , , , , , , , , , ,


Monday, March 1, 2010

 

Responding to zooms and pans in Android's MapView

There's a curious lacuna in Android's MapView API: it's (relatively) easy to display a map, and overlay items atop a map; it's (relatively) easy to detect when an overlay item has been touched; but there's no straightforward way to work out when the user has touched anywhere else on the map. Because of the way MapView works, the usual onTouchListener() solution only works for one touch, and then fails.

There is, however, a solution. Not a pretty one, but it works. The solution is to use your own subclass of MapView:


public class ITRMapView extends MapView {
public ITRMapView(android.content.Context context, android.util.AttributeSet attrs) {
super(context, attrs);
}

public ITRMapView(android.content.Context context, android.util.AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}

public ITRMapView(android.content.Context context, java.lang.String apiKey) {
super(context, apiKey);
}
}


and within that subclass, override onTouchEvent. You're probably only interested in the UP action:

public boolean onTouchEvent(MotionEvent ev) {
if (ev.getAction()==MotionEvent.ACTION_UP) {
//do your thing
}
return super.onTouchEvent(ev);
}


But what if you want to detect zoom actions as well as touches? Simple, thought I: just override onDraw() and do the same thing. But you can't - MapView's onDraw() is final!

Catastrophe? Not quite. It turns out that overriding dispatchDraw() works just fine:

int oldZoomLevel=-1;

public void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
if (getZoomLevel() != oldZoomLevel) {
//do your thing
oldZoomLevel = getZoomLevel();
}
}


Et voila - you can programmatically detect and respond when the user pans and/or zooms your map. Really not so hard after all.

Labels: , , , , , , , , , ,


Tuesday, September 1, 2009

 

iPhone bits and bobs

Hello there, O my droogs. Long time no talk. I've been working on an iPhone app for pay, you see, which I figure kind of limits how much code I can provide you. I do have a few notes that might be worth sharing, though:

Labels: , , , , , , , ,


This page is powered by Blogger. Isn't yours?

Subscribe to Posts [Atom]