Custom Map Renderer: iOS

It Finally Happened. I picked up my first Mac computer in January. I am now the proud owner of a MacBook Pro 13, no touch bar and the regular keyboard. I went into the local Apple Store and tried out the newer keyboard and the older keyboard and have to say that I liked the feel of the old one better. I really didn’t like how loud the new keyboard was. But the old keyboard was good, trackpad was good (but not as good as I thought it was going to be) and the screen was nice. Overall a nice computer if expensive. But the important thing is that I could finally install XCode and build iOS apps!

And that is the subject of the this blog post. In the previous blog post, we looked at creating a custom renderer for Android. Now we will look at doing the same with iOS. With iOS, things are simpler in at least one regard: you don’t have to go get an access key for your app.

One thing I want to make clear after some questions from others. This information isn’t a whole lot different than what you can get from developer.xamarin.com. But it builds on those examples by making the custom map renderer updatable from your view model … if you are using data binding of course. If you aren’t using data binding, you can skip all of this and just follow the guide in the above link.

If you are still here …

I didn’t change anything in my CustomMap class in the shared project. All updates are in the iOS project. In here I have defined two classes: CustomMKAnnotationView and CustomMapRenderer. Let’s talk about CustomMKAnnotationView class first: it is the simpler.

The CustomMKAnnotationView class derives from MKAnnotationView. It adds a couple of new properties for the Id of the marker and a URL associated with the marker. Otherwise there is not a lot to be excited about. So lets move onto the CustomMapRenderer class.

The CustomMapRenderer class derives from MapRenderer. The first thing is that we have to override the OnElementChanged method. Just like Android, this either hooks up the native map to the cross platform map class in the shared project or unhooks it.


protected override OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.View> e)
{
    base.OnElementChanged(e);

    if (e.OldElement != null)
    {
        //... unhook events, native conrols etc
    }

    if (e.NewElement != null)
    {
        _customMap = (CustomMap) e.NewElement;
        _nativeMap = Control as MKMapView;
        _customPins = _customMap.CustomPins;

        _nativeMap.GetViewForAnnotation = GetViewForAnnotation;
        _nativeMap.CalloutAccessoryControlTapped +=
            OnCalloutAccessoryControlTapped;
        _nativeMap.DidSelectAnnotationView +=
            OnDidSelectAnnotationView;
        _nativeMap.DidDeselectAnnotationView +=
            OnDidDeselectAnnotationView;
    }
}

The most important things above are that you get a reference to our CustomMap object that was defined in the shared project; and also get a reference to the native map object (MKMapView).

Using the native map object from above, we can hook into events for it. We use these events to display the pins on the map and the info window when a user taps one of the pins.

Let’s take a look at the GetViewAnnotation method. This is the method that is used to show a custom marker on the map. In it we are passed in a reference to the marker. We use our GetCustomPin method to look up the information associated with the marker. In an effort to conserve memory, the map can reuse the view: and that is what is happening with the call to DequeueReusableAnnotation. If we can’t reuse, then we create a new one, specifying the image for the pin itself, and customizations for the callouts.


private MKAnnotationView GetViewForAnnotation(
    MKMapView mapView, IMKAnnotation annotation)
{
    if (annotation is MKUserLocation)
        return null;

    MKAnnotationView ret = null;

    var pointAnnotation = annotation as MKPointAnnotation;
    var customPin = GetCustomPin(pointAnnotation);
    if (customPin == null)
        throw new Exception("My pin not found");

    ret = mapView.DequeueReusableAnnotation(customPin.Id.ToString());
    if (ret == null)
    {
        ret = new CustomMKAnnotationView(
            annotation,
            customPin.Id.ToString(), customPin.Url);
        ret.Image = UIImage.FromFile("custompin2.png");
        ret.CalloutOffset = new CoreGraphics.CGPoint(0, 0);
        ret.LeftCalloutAccessoryView =
            new UIImageView(UIImage.FromFile("custompinimage.png"));
        ret.RightCalloutAccessoryView =
            UIButton.FromType(UIButtonType.DetailDisclosure);
        ((CustomMKAnnotationView) ret).Id = customPin.Id.ToString();
        ((CustomMKAnnotationView) ret).Url = customPin.Url;
    }

    ret.CanShowCallout = true;

    return ret;
}

Next we can take a quick look at the OnDidSelectAnnotationView. This is called when the user taps on the pin. You have the option of doing something custom … or just select the default that was setup in the GetAnnotationView function. In our case, we are going to see if the marker is associated with ID 1, and if it is, add in a custom image. Otherwise, we are just going to accept the default.


private void OnDidSelectAnnotationView(
    object sender,
    MKAnnotationViewEventArgs e)
{
    var customView = e.View as CustomMKAnnotationView;
    _customPinView = new UIView();

    if (customView.Id == "1")
    {
        _customPinView.Frame = new CoreGraphics.CGRect(0, 0, 200, 84);
        var image = new UIImageView(new CoreGraphics.CGRect(0, 0, 200, 84));
        image.Image = UIImage.FromFile("custommapimage.png");
        _customPinView.AddSubview(image);
        _customPinView.Center =
            new CoreGraphics.CGPoint(0, -(e.View.Frame.Height + 75));
        e.View.AddSubview(_customPinView);
    }
}

The OnDidDeselectAnnotationView method is called when the extended annotation is displayed and the user taps somewhere on the map, it will get rid of the extended information and free up the resources.


private void OnDidDeselectAnnotationView(
    object sender,
    MKAnnotationViewEventArgs e)
{
    if (!e.View.Selected)
    {
        _customPinView.RemoveFromSuperview();
        _customPinView.Dispose();
        _customPinView = null;
    }
}

Finally, let’s take a look at OnCalloutAccessoryControlTapped. After the user taps on the pin, extra data is displayed about the pin. If you tap on the extra data, this function is called. You can look at the information associated with the pin and act on it. In our case, we are going to grab the url and navigate to it.


private void OnCalloutAccessoryControlTapped(
    object sender,
    MKMapViewAccessoryTappedEventArgs e)
{
    var customView = e.View as CustomMKAnnotationView;
    if (!String.IsNullOrWhiteSpace(customView.Url))
    {
        UIApplication.SharedApplication.OpenUrl(
            new Foundation.NSUrl(customView.Url));
    }
}

And now just for a recap. If we want to make the custom map respond to our view model we have to listen for changes from the data binding system. This is almost exactly the same as the Android implementation where we override the OnElementPropertyChanged and listen for our property.


protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
    base.OnElementPropertyChanged(sender, e);

    if (Element == null || Control == null)
        return;

    if (e.PropertyName == View.CustomMap.CustomPinsProperty.PropertyName)
    {
        UpdatePins();
    }
}

private void UpdatePins()
{
    if (_nativeMap == null)
        return;

    foreach (var p in _customMap.CustomPins)
    {
        MKPointAnnotation pa = new MKPointAnnotation
        {
            /// check git hub
        }

        _nativeMap.AddAnnotation(pa);
    }
}

Don’t forget, the CustomPins is bound in our XAML page.

I find it pretty amazing that Xamarin Forms is able to share so much of the code. Really all we are doing in the separate projects is the presentation: all of the business logic is shared between all. Very cool.

Find all the code in my GitHub under the XamarinPrism repository. Find it under the MapRendererIos solution.

Xamarin Forms Map Center

Problem:

I have been working on an app in Xamarin, with the goal of being able to deploy to UWP, Android and IOS. This is my first real effort with Xamarin, and the first time publishing IOS and Android to their respective stores. Actually, this is my first IOS app ever! I still need to get on to Craigslist and find a reasonably priced Mac Mini so that I can build the IOS app.

Some app background: it makes use of the map control. My needs aren’t too difficult, but I need to know what the map control is displaying at any given moment. Specifically, I need the latitude and longitude of the map center. At least at this point, my app doesn’t need to worry about location tracking.

I am going to use Xamarin forms and take advantage of the cross platform UI. I also want to make use of the MVVM pattern and the data binding associated with XAML.I will of course use my favorite MVVM framework: Prism (I am going to do a series on Prism and UWP and a series on Prism and Xamarin Forms as well at some point). All of this sounds great: but unfortunately the map center data structure isn’t exposed as a bindable property, so we can’t use it directly in our view model.

Well, since every C#/XAML blog person needs an attached property post, that is what I decided to do. I am not sure that it is the best solution, but I can say that it seems to be working for me right now. Look for an update on this post if it doesn’t!

Solution:

I decided I wanted two properties. One that turned the tracking on and off and one that stored the information about what the map was viewing.

Attached properties are all kind of the same, so first we will just setup the basics (if you need a bit of a refresher on attached properties, check out the Xamarin docs):

  • Creation
  • Getter/Setter
  • Change event
public static class MapCenterProperty
{
	public static readonly BindableProperty CenterProperty =
		BindableProperty.CreateAttached(
			"Center",
			typeof(MapSpan),
			typeof(MapCenterProperty),
			null,
			propertyChanged: OnCenterChanged);

	public static MapSpan GetCenter(BindableObject view)
	{
		return (MapSpan) view.GetValue(CenterProperty);
	}

	public static void SetCenter(
		BindableObject view, MapSpan mapSpan)
	{
		view.SetValue(CenterProperty, mapSpan);
	}

	private static void OnCenterChanged(
		BindableObject view, object oldValue, object newValue)
	{
	}

	public static readonly BindableProperty TrackCenterProperty =
		BindableProperty.CreateAttached(
			"TrackCenter",
			typeof(bool),
			typeof(MapCenterProperty),
			false,
			propertyChanged: OnTrackCenterChanged);

	public static bool GetTrackCenter(BindableObject view)
	{
		return (bool) view.GetValue(TrackCenterProperty);
	}

	public static void SetTrackCenter(
		BindableObject view, bool value)
	{
		view.SetValue(TrackCenterProperty, value);
	}

	private static void OnTrackCenterChanged(
		BindableObject view, object oldValue, object newValue)
	{
	}
}

So let’s take a look at the TrackCenterProperty first. This is the one that is used to toggle the tracking off and on. The important stuff happens in the OnTrackCenterChanged event: it sets up the tracking. If the old value is false and the new value is true, start tracking. Stop tracking if the reverse is true.

Now it would be great if the Map control had an event that fired when the view of the map changed, but it doesn’t. So that is a bit of an issue. Luckily there is a bit of a hack we can use. We are interested in when the “VisibleRegion” property changes. So we are going to subscribe to the “PropertyChanged” event instead. If the name of the property being changed is “VisibleRegion”, we want to know about it.

Let’s update the OnTrackCenterChanged method from up above to subscribe and unsubscribe depending on the value of TrackCenter. It could look like the following:


private static void OnTrackCenterChanged(
	BindableObject view, object oldValue, object newValue)
{
    Map map = view as Map;
    if (map == null)
        return;

    bool ov = (bool) oldValue;
    bool nv = (bool) newValue;

    if (ov && !nv)
    {
        /// was true, now false, unsubscribe
        map.PropertyChanged -= OnMapPropertyChanged;
    }
    else if (!ov && nv)
    {
        /// was false, now true, subscribe
        map.PropertyChanged += OnMapPropertyChanged;
    }
}

All we are doing is subscribing or unsubscribing to the PropertyChanged event depending on the value of TrackCenter. Below is the event handler for that event. The only thing it really does is check to see if it is the VisibleRegion property being changed and if so, update the value of the CenterProperty.


private static void OnMapPropertyChanged(
	object sender, PropertyChangedEventArgs a)
{
    Map map = sender as Map;
    if (map == null)
        return;

    if (a.PropertyName == "VisibleRegion"
        && map.VisibleRegion != null)
    {
        Debug.WriteLine(&quot;SetCenter&quot;);
        map.SetValue(CenterProperty, map.VisibleRegion);
    }
}

Now all we have to do is setup the OnCenterChanged event. The only thing we have to be concerned about is giving ourselves a bit of fuzziness on whether the center point is changed enough to update the map, otherwise I found that the center point property was being updated too often.

private static void OnCenterChanged(
    BindableObject view, object oldValue, object newValue)
{
    var map = view as Map;
    if (map == null)
        return;

    MapSpan newview = newValue as MapSpan;
    MapSpan oldView = oldValue as MapSpan;

    if (newview != null &&
        !Util.MapSpanHelper.IsEqual(oldView, newview))
    {
        map.MoveToRegion(newview);
        SetCenter(view, newview);
    }
}

And the helper function for checking if the MapSpan has changed enough:

public static class MapSpanHelper
{
    public static bool IsEqual(double d1, double d2)
    {
        double dif = Math.Abs(d1 * 0.00001);
        return (Math.Abs((d1 - d2)) <= dif);
    }

    public static bool IsEqual(MapSpan ms1, MapSpan ms2)
    {
        if (ms1 == null && ms2 == null)
            return true;
        else if (ms1 == null || ms2 == null)
            return false;
        else if (
            IsEqual(ms1.Center.Latitude, ms2.Center.Latitude)
            && IsEqual(ms1.Center.Longitude, ms2.Center.Longitude)
            && IsEqual(ms1.LatitudeDegrees, ms2.LatitudeDegrees)
            && IsEqual(ms1.LongitudeDegrees, ms2.LongitudeDegrees))
            return true;
        else
            return false;
    }
}

You can adjust the fuzziness as appropriate.

Finally, we can setup our XAML and view model. Remember to add the namespaces. The MapCenterProperty class exists in the “Demo.Views” assembly.


xmlns:maps="clr-namespace:Xamarin.Forms.Maps;assembly=Xamarin.Forms.Maps"

xmlns:loc="clr-namespace:Demo.Views;assembly=Demo"

<maps:Map MapType="Street" loc:MapCenterProperty.TrackCenter="True" loc:MapCenterProperty.Center="{Binding Center,Mode=TwoWay}">

</maps:Map>

And the view model:


/// BindableBase provided by Prism, implements the
/// INotifyPropertyChanged interface for the view model
/// and some helper functions
public class DemoViewModel : BindableBase
{
    // ...

    private MapSpan _center = null;
    public MapSpan Center
    {
        get { return _center; }
        set { SetProperty<MapSpan>(ref _center, value); }
    }
}

And there we have it. We now have the viewable region of the map control bound to our view model. From within the view model we can change what is being shown on the map simply by setting the Center property. In the case of my app, I am also going to make use of the GeoLocator plugin from James Montemagno to provide some pretty nice location services to go with the maps in my app.

Hopefully you found this useful.