Falko Falko - 1 month ago 26
iOS Question

iOS MKMapView with overlay view catching selected touches/gestures

I'm trying to implement an MKMapView with an overlay view, that draws an editable polygon. Therefore I need to selectively catch touch gestures targeting vertices, but let through other touches/gestures in order to allow scrolling and zooming the map.

So how can I programmatically send selected touches either to the map or to the overlay, depending on their coordinates?

The following code boils it down to a simplified scenario: An overlay view is colored red on the left hand side and blue on the right hand side. One side should catch touch events and the other side should allow map interactions.

Both, map and overlay, are subviews on a

SuperView
:

public sealed class SuperView: UIView
{
readonly UIView map = new MapView();
readonly UIView overlay = new Overlay();

public SuperView()
{
AddSubview(map);
AddSubview(overlay);
}

public override UIView HitTest(CGPoint point, UIEvent uievent)
{
return point.X < Frame.Width / 2 ? map : overlay;
}
}


The
MapView
derives from
MKMapView
and only makes sure to allow scrolling:

public sealed class MapView: MKMapView
{
public MapView() : base(UIScreen.MainScreen.Bounds)
{
ScrollEnabled = true;
}
}


The
Overlay
contains a gesture recognizer and visualizes the two zones:

public sealed class Overlay: UIView
{
public Overlay() : base(UIScreen.MainScreen.Bounds)
{
BackgroundColor = new UIColor(0, 0, 0, 0);
AddGestureRecognizer(new Recognizer());
}

public override void Draw(CGRect rect)
{
using (var context = UIGraphics.GetCurrentContext()) {
context.SetFillColor(new CGColor(1, 0, 0, 0.25f));
context.FillRect(new CGRect(rect.Left, rect.Top, rect.Width / 2, rect.Height));
context.SetFillColor(new CGColor(0, 0, 1, 0.25f));
context.FillRect(new CGRect(rect.Width / 2, rect.Top, rect.Width / 2, rect.Height));
}
}
}


The recognizer is straight forward:

public sealed class Recognizer: UIGestureRecognizer
{
public override void TouchesBegan(NSSet touches, UIEvent evt)
{
Console.WriteLine("TouchesBegan");
base.TouchesBegan(touches, evt);
}

public override void TouchesMoved(NSSet touches, UIEvent evt)
{
Console.WriteLine("TouchesMoved");
base.TouchesMoved(touches, evt);
}

public override void TouchesEnded(NSSet touches, UIEvent evt)
{
Console.WriteLine("TouchesEnded");
base.TouchesEnded(touches, evt);
}

public override void TouchesCancelled(NSSet touches, UIEvent evt)
{
Console.WriteLine("TouchesCancelled");
base.TouchesCancelled(touches, evt);
}
}


The resulting screen looks as follows:

enter image description here

When touching the red section, I'd wish to interact with the map - which doesn't work.
When touching the blue section, I want to see console output from the gesture recognizer - which does work indeed.

What doesn't work:


  • Resizing the overlay to only half of the screen.

    In this artificial example this would help. But in my real-life app the distinction between touch and no touch is more complex.

  • Adding the gesture detector to the
    MapView
    .

    Since each gesture recognizer will receive touch events in a non-deterministic order, you never know whether the map or the gesture detector receives a touch event first. So the map might be scrolling already even if the touch shouldn't have received the map.

  • Overriding
    HitTest
    or
    PointInside
    .

    I tried overriding these methods in
    SuperView
    ,
    MapView
    and/or
    Overlay
    . But they are either not called or seem to have no effect. As least I can prevent the gesture recognizer from receiving touches on the red area. But it doesn't reach the map as expected. Maybe I did something wrong. But it seems like the map ignores forwarded touches.


Answer

I seem to have solved the problem described above. Apparently, the MKMapView shouldn't be a subview, but need's to be the View of ViewController:

public sealed class SuperView: UIViewController
{
    readonly UIView map = new MKMapView();
    readonly UIView overlay = new Overlay();

    public SuperView()
    {
        View = map;
        map.AddSubview(overlay);
    }
}

Then the Overlay view gets the HitTest implementation:

    public override UIView HitTest(CGPoint point, UIEvent uievent)
    {
        return point.X < Frame.Width / 2 ? null : base.HitTest(point, uievent);
    }