It’s yet again time to write an article in English. This time about some iPhone development dilemma I stumbled upon lately working for my bachelor thesis. As you may know the iPhone Licence (or whatever) doesn’t allow developers to use Google Maps (called MapKit in the SDK) for – as they call it – “Turn by Turn”-navigation applications. I actually don’t want to do this but I’d still like to draw a route into the map. Sadly Apple obviously believed that the only reason one would want to do this would be to provide such a thing. Therefore they didn’t include an abstraction to easyly accomplish that. Yet I still wanted it and this is how I got it to work (with transitions and animations!).
The first thing that came to my mind was to put a non-opaque UIView above the MKMapView and to draw my route into that view. I would simply use convertCoordinate:toPointToView: to get the right points from the map mapped to my overlaying view. At first thought a good idea but it suffers from two serious problems. First (and worse) is the event tracking. You might think it would be easy to intercept events and implement the 4 methods (touchesBegan:… and so on) to move your route and then propagate the events to the underlying mapview but it isn’t. I really tried a lot things and googled up some stuff that sent messages directly to some subview of the MKMapView (which is a hack, of course) but the best thing i could come up with, was moving with your fingers down working but at the same time breaking the “pinch” for zooming which rendered this method pretty useless. Another problem was that you could scroll by sliding your finger on the screen fast and then let go which will let the map scroll further but no more events are fired as no finger is on screen. You could in a way combine this with handling the map-delegate methods didChangeRegion and willChangeRegion but it still won’t work the way you want it. Especially because willChangeRegion does fire somewhat unexpectedly and not constantly while scrolling or zooming in. Second thing thats wrong with this approach is that you will draw the route above the whole map layer which is unusable if you want to use the annotations of the map, too. To sum it up. This approach is crap. I really spend a lot of time trying stuff (even with timers redrawing the route…) but you can’t get it to work the way you want it.
After a little bit of more googleing I found this site of Craig who actually proposed the some approach but had posted an update where he suggested to draw the route into a custom annotation-view to solve the problem with the covered annotations. I liked that idea and tried some stuff on my own and finally this is how you get it done:
Create your own MKAnnotationView subclass which follows the MKAnnotation Protocol, too. Create your own UIView subclass on which you will draw the route later on. Your MKAnnotation subclass creates on initilization a new instance of your UIView subclass and pushes the route-points (e.g. array of CLLocationCoordinate2D) to this internal view. It also needs to set itself to not clipsToBounds, also set it’s bg-color to clearColor and opaque to NO. You also need to push a reference to the MKMapView to the internal view. The last thing to do is to add this internal view as a new subview to your custom MKAnnotation. Btw. you annotation can have a size of 0×0 as we don’t clip our subview and this is the one the route will be drawn upon.
Then you’ll implement the MKAnnotation method coordinate which will return [yourMapViewReference centerCoordinate]. This way – if added to the map – your custom annotation will always be positioned at the center of the map. Regardless of scrolling or zooming. This is important as the MKMapView will remove you route if the annotation is not currently visible (positioned) on the visible region of the map. Then create some method that will order the internal subview to redraw itself and reposition it to the currently visible rect of the map. Basicly it will look like this:
-(void) initRedraw { CGPoint origin = CGPointMake(0, 0); origin = [mapView convertPointrigin toView:self]; internalView.frame = CGRectMake(origin.x, origin.y, mapView.frame.size.width, mapView.frame.size.height); [internalView setNeedsDisplay]; }
Then you want to fire up that method in regionDidChangeAnimated in your MKMapViewDelegate. But this won’t do the trick entirely because it will only redraw the map when the region did change but we want to redraw constantly while we’re zooming and scrolling. Maybe you will also note that your internal view isn’t positioned correctly. This is because MapKit will first fire regionDidChangeAnimated then your redrawing will take place then MapKit will ask the your custom annotation for it’s corrdinate and will finally reposition it which you won’t note because there is no event or delegate method for that and this will result in your view beeing repositioned as well as your route. The trick is now to overwrite setCenter: in your custom MKAnnotation. As following the calls above your view will be repositioned using setCenter:. You can overwrite it and abuse it as event listener for any motion (zooming, pinching, scrolling) in the map by firing your initRedraw method in that overwritten thing (remember to call the super-method). If your drawRect in your internal subview is efficiently written and you don’t have too much route points this will allow great performance on 3GS hardware and acceptable performance on first-gen hardware. I didn’t optimize the whole thing and it works great for me with ~ 150 Points (even more on 3GS).
Actually I’m not sure about if you would really need the interal subview to draw upon but I took this idea from Craig’s site. You could also try to draw on your MKAnnotationView directly. I hope some of you can use this but remember not to break the rules Apple made on using MapKit. Also I’d like to thank Craig for his idea with the custom annotation.
Finally, I think this is the best approach to implement routes or polylines into MapKit. I’m currently too busy to put up an example project but if you read Craig’s posts and mine as well you should get it running. If someone is willing to create some abstraction for the MKAnnotation or maybe even a full MapViewController I will happily link it here. Comments are welcome. Good luck with your own projects.
Tags: apple, drawing, google maps, iphone, mapkit, polylines, routes, transitions
Thanks for the link! I’m glad you found the article useful.
Hi Nicolas
Interesting article – I think you might have found the solution to a problem that many developers working with MapKit face
Would you mind to clarify a few things? There are a few things in your article that are unclear to me, and it would really help if you could address these. In particular, it seems to me that there is some confusion about when you mean MKAnnotation and when you mena MKAnnitationView. For example, you say “The last thing to do is to add this internal view as a new subview to your custom MKAnnotation. Btw. you annotation can have a size of 0×0 as we don’t clip our subview and this is the one the route will be drawn upon.” This seems strange – the custom MKAnnotation implements MKAnnotation, and is not a view itself – so how can you add the internal view as a subview?
Similarly, you suggest to overwrite setCenter of MKAnnotation (”The trick is now to overwrite setCenter: in your custom MKAnnotation.”). But MKAnnotation doesn’t have a setCenter method – MKAnnotationView has.
If you could clarify this in your post that would be great, and if I get this to work I’d be happy to send and example projects for others to check out.
Thanks!
Marcel
Hi Marcel,
I guess I should have read the article again before publishing it. I made a mistake in the line that says “Create your own MKAnnotationView subclass which follows the MKAnnotation Protocol, too.”. In the original version it said MKAnnotation (without the view) twice, which is wrong of course.
What you do is create indeed an MKAnnotation**VIEW** subclass which also is an MKAnnotation itself. This class will get the custom UIView as subview you can draw your route on, this class can have a size of 0×0 (if it doesn’t clip the just mentioned subview). And in this class you will overwrite setCenter:. Make sure you will add this custom MKAnnotationView to the annotations of the MKMapView and also (!) to return itself (as it is the view as well as the annotation) if the delegate get’s asked for it.
Thank you for your feedback. I’m looking forward to seeing your project and really would like to link it here, as is propably will help a lot more than my (a assume a bit cryptic) writing.
Best Regards,
Nicolas
Hi Nicolas
Thanks for the clarification. I just now managed to get it to work!
There are still a number of sentences where you refer to the MKAnnotation instead of MKAnnotationView, but hey, I got it to work, so what
I’ll send you the link as soon as I can.
Cheers
Marcel
Basicly, after having clearified that your MKAnnotationView and MKAnnotation are the same class it shoudn’t matter… But I guess I’ll go over the text again the next days. Looking forward to your project and glad that I could help
Hi,
did you ever end up doing an example project? I am having trouble with my CSRouteView (this is from the original code Craig provided) “moving” around when I zoom in. Rather, the physical area on the map covered initially is reduced as I zoom in and I have a feeling this might be what you’re talking about. I tried overriding the setCenter and calling regionChanged (= your initRedraw), but that didn’t help.
Thanks in advance!
Best,
Jacob
Hey,
I’m not sure if I fully understand your problem. If your CSRouteView is an MKAnnotationView, setCenter: will be called when you move within the map. Also (I’m not 100% sure about this since it has been quite long) it should be called when zooming so you would want to redraw your route on setCenter: to abuse this as a event-listener.
Hope this helps.
Nicolas
Hi,
thanks for your quick response
It’s hard to explain, but see the pics below. The black box is the area of the CSRouteView, i.e. also the view that receives the touch events (can’t get the underlying internal map view to receive touches :-/):
Screen #1 – initial view when app starts, my custom detect touch in polygon function works fine as the polygon (the green one) is inside the black area.
Screen #2 – after a couple of zooms the black area suddenly seems to get close to the edge of the polygon (detection still works).
Screen #3 – one more zoom and the polygon is no longer completely covered by the black area and thus doesn’t receive touch-events in the non-black area.
For me I would think the black area should at screen #3 completely surround the screen as it should cover the same area on the map as screen #1.
Hope that explains it?
Best and thanks for your quick response,
Jacob
If your “underlying” map view, as you said above, can’t receive touch events, you’re not using the approach I’m describing in the blog post. In my (or Chris) approach you will not put anything above the maps view, you will only insert the routes as annotations and will not intercept any touch events. If you want to intercept touches you might want to use the delegation method for annotations (which I don’t remember currently).
I’d like to give you further advice but I think the basic problem is, that you are putting views above your map view which is (as I stated above in the blog entry) a bad idea if you want to archive fluid zooming and scrolling with animations.
Greetings,
Nicolas
Thanks for the article. If Apple & Google prohibits the use of their routinginformation is there any other known source where to get routinginformation from?
Cloudmade is far way too expensive. Would apple accept an app with this kind of route? I think it depends on the “day”. taxometer just submitted an app using this technique. Mhh.
I’m not a lawyer so I don’t know what exactly Apple is prohibiting. I understand that you are not allowed to draw routes on the internal MapKit Views for navigational purposes. Wherever your routing information comes from. I think OpenStreetMaps offers (free) possibilities to add routing information (and MapViews) to iPhone Applications but I didn’t use those yet so I can’t give you advice.
[...] would disappear when the user is performing a zoom action on the map). Nicolas Neubauer later expanded upon Craig’s idea with the idea of hooking into the annotation view’s center property [...]