Sunday, 6 September 2009

MKMapView overlay

This short blog post will be for iPhone OS v.3.0 programmers who use MapKit framework for displaying maps in their iPhone applications.


I faced a problem with MapKit which other programmers on the internet also seem to have, but I couldn't find a solution anywhere. But let's start from the beginning:


Why I have encountered the problem

I wanted to display map scales on top of MKMapView, so I have created MapScales subclass of UIView. My MapScales class has a reference to MKMapView and whenever it is displayed (drawRect is called), it checks the distances on the currently shown rectangle on MKMapView and draws nice scales. I made my MapScales instance semi-transparent and put it on top of MKMapView in my hierarchy of UIViews. Next I wanted my MapScales to be dispayed only when at least two user's fingers are on the map - so that the scales are shown only when the user pretends to zoom in or out. So I had to intercept the touch events in my MapScales. And here is:


The problem

MKMapView doesn't work correctly when it is not the first interceptor of touch events.


What happens:

  • Some of the standard animations in MKMapView are not working any more.
  • MKMapView is still usable, but it doesn't react to standard user interactions in a normal way any more.

When exactly it happens:

  • When some other component (like my MapScales) intercepts the event first, and then sends it to MKMapView.
  • It doesn't matter of the other component (MapScales) is a subview of MKMapView or a subview of MKMapView's superview.
  • It doesn't matter if the other component is sending events directly to MKMapView's instance or to the object obtained by calling hitTest:withEvent: on MKMapView's instance.
  • I also tried to fix the problem by making my MapScales a subview of MKMapView, but I couldn't get a satisfactory solution.

I don't know what causes this problem, but I suspect that MKMapView's behaviour may depend on UITouch'es read-only view field, which will be different whenever MKMapView is not the first event interceptor.


Well this problem is so ugly, that it invalidates the benefits of creating MapScales in my application.


My solution

It is not very clean solution, but it let's you create the MKMapView overlay that intercepts the events and doesn't spoil MKMapView's behaviour.

Before the events are distributed to UIView hierarchy, they first get to UIWindow, so I have created a UIWindow subclass (MyMainWindow) and made the main window of my application an instance of MyMainWindow. Then inside MyMainWindow I have implemented the method:


- (void)sendEvent:(UIEvent*)event {

[super sendEvent:event];

[listeners sendEvent:event];

}


As you can see I have also implemented a simple mechanism of observing MyMainWindow (listeners attribute). Then I have registered my MapScales instance as MyMainWindow's listener.


Of course I also had to disable user interaction flag in my MapScales instance, as now it is getting events from different source.


This solution has some disadvantages. For example, MapScales always gets all touch events, even when the user touches something else than the map - this is the price of skipping standard event delivery mechanism. Though it works very well for me.


If you find a better solution, please let me know.

16 comments:

  1. How are you able to add a subview to mapView ?

    ReplyDelete
  2. Hi Ben,

    As MKMapView is a subview of UIView, you can use UIView's -(void)addSubview:(UIView*) method.

    ReplyDelete
  3. Hi Krout,

    I was searching a solution for a while about events on Mkmapkit.

    Can you also add some source code showing how you add the MapScales?

    Thanks for the solution!

    Tony

    ReplyDelete
  4. How did you create the MyMainWindow?

    ReplyDelete
  5. Hi everyone,

    Unfortunately I cannot show you the code of MapScales solution as I've sold the rights to it to one of my customers.

    In terms of creating MyMainWindow... in the Interface Designer open your main xib file, you should have Main Window object in it. Go to this object's Identity Inspector (In the Tools menu) and change the class name to MyMainWindow. Then save the xib file and start your application - your main window will be an instance of MyMainWindow class.

    Best Regards,
    KC

    ReplyDelete
  6. Thanks, I had instanced the main window of the application with MyMainWindow.

    In the MyMainWindow:

    - (void)sendEvent:(UIEvent*)event {
    [super sendEvent:event];
    [Mapscales sendEvent:event];
    }

    In the Mapscales, I added the function to detect the event:

    - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    return self;
    }


    - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
    {
    NSLog(@"Moved");
    }


    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    NSLog(@"Touch Began");
    }


    - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    NSLog(@"Touch Ended");
    }

    - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
    NSLog(@"Touch Cancel");
    }

    But when I do that, I also lose the control on the map..

    What did I miss?

    ReplyDelete
  7. Hi,

    I guess that the problem is that you still capture events through the standard event mechanism - see your implementation of hitTest. You should remove all standard event handling methods in your MapScales implementation and turn off user interaction in the MapScales instance. Then you can send events from MyMainWindow to your MapScales instance via the sendEvent method - you have to implement this method in MapScales.

    I hope this helped.

    KC

    ReplyDelete
  8. Hi KC,

    Thanks a lot for your help!

    It works very well now!

    Thx!

    Brian

    ReplyDelete
  9. What should the implementation of sendEvent in MapScales look like? It is not part of UIView.

    ReplyDelete
  10. My contribution to this question is the missing code:

    As told implement UIWindow to catch the events. Then send the events to your customized view you want the events also to be received. Then there implement a receiver like this:


    - (void)sendEvent:(UIEvent*)event
    {
    NSLog(@"got event ",[event type]);

    if (event.type==UIEventTypeTouches)
    {
    NSSet *set = [event allTouches];

    NSArray *ar = [set allObjects];

    for (int i=0; i<[ar count]; i++)
    {
    UITouch* touch = [ar objectAtIndex:i];

    if (touch.phase == UITouchPhaseEnded)
    {
    [**destinationview** touchesEnded:[ar objectAtIndex:i] withEvent:event];
    }
    }

    }



    }

    ReplyDelete
  11. Hosted business phone system is an ideal solution for business now. You don't have to worry about installing any software or hardware. However, can have access to all the features large business have. The best part is the price is very affordable. It starts from about $10/month. With this system you don't need a separate business phone line. You can use your personal cell phone or home phone. You will sound as professional as fortune 500 companies. I have been using toll free numbers from Telcan. I believe they provide the Toll Free Numbers at the best price. They even gurantee to beat any lower price by 10%.

    ReplyDelete