0 Comments

00_TitleImage

Ever had to develop an app where the position of something was of interest. An address of a user, store, restaurant or some other point of interest? The straight forward version might be to provide the user with a simple form.

Address entry form

While the form is easy to implement and reliable, it can be cumbersome. Plus what if you want to navigate to the location? Or why not use the position of the device? Tap on a map to get the address? Well, all of these things can be achieved quite easily with a few helpful libraries provided by the Xamarin team.

Geocoding like a pro

Whenever you need to convert between coordinates and addresses, Geocoding is your best friend. You can show up with either an address or position, and you will get back the opposite:

var location = (await Geocoding.GetLocationsAsync($"{street}, {city}, {country}")).FirstOrDefault();

if (location == null) return;

Position = new Position(location.Latitude, location.Longitude);

So if we had a form where the user can enter the address and we wanted to navigate to that position, we could convert the address to geo coordinates and then, using Xamarin Essentials, open the maps app on the platform and navigate to that position.

var location = new Location(Position.Latitude, Position.Longitude);
var options = new MapLaunchOptions { NavigationMode = NavigationMode.Driving };
await Xamarin.Essentials.Map.OpenAsync(location, options);

Quite impressive for a bunch of entries and a few lines of code right?

Getting an address with one tap

But typing in addresses can be cumbersome. Why not just select the position on the map and get the address from that position? Xamarin Forms has supported the maps control for quite some time. Once you have added the NuGet package, it will allow you to present the native map platform, i.e. Google Maps on Android.

<?xml version="1.0" encoding="utf-8" ?>
             ...
             xmlns:maps="clr-namespace:Xamarin.Forms.Maps;assembly=Xamarin.Forms.Maps"
             ... >

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="2*" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <maps:Map x:Name="MapControl" />
        ...
    </Grid>

</ContentPage>


Note there are some setup steps required depending on the platform you intend to target. So be sure to check out the official docs from Microsoft on those details. Here we will continue to focus on the fun stuff

The map has an event MapClicked which fires when the user taps on the map with the geo-position.

Observable.FromEventPattern<MapClickedEventArgs>(
    mc => MapControl.MapClicked += mc, 
    mc => MapControl.MapClicked -= mc)
    .Subscribe(ev => ViewModel.ExecuteSetAddress.Execute(ev.EventArgs.Position));

If you are not familiar with Rx.Net, here is the equivalent code with an event handler:

protected override void OnAppearing()
{
   // ...

    MapControl.MapClicked += HandleMapClicked;
}

private void HandleMapClicked(object sender, MapClickedEventArgs e)
{
    var postion = e.Position;
    // ...
}

To give the user some feedback, we could even present the taped position with a pin:

private Unit SetPin(Position position)
{
    Pin pin = new Pin
    {
        Label = "The Place",
        Address = $"{ViewModel.Street}, {ViewModel.City}, {ViewModel.Country}",
        Type = PinType.Place,
        Position = position
    };

    var latDegrees = MapControl.VisibleRegion?.LatitudeDegrees ?? 0.01;
    var longDegrees = MapControl.VisibleRegion?.LongitudeDegrees ?? 0.01;
    MapControl.MoveToRegion(new MapSpan(position, latDegrees, longDegrees));
    MapControl?.Pins?.Clear();
    MapControl?.Pins?.Add(pin);
    return Unit.Default;
}

This code will always place the pin at the new location. If the user taps the pin, it will display the information provided to the pin.

Side note: Managing to present that info is quite a challenge given this code, 9 out of 10 times I only managed to place the pin at a new position. But perhaps that's just me

Using the geocoder, we can now get the address for those coordinates:

private async Task SetAddress(Position p)
{
    var addrs = (await Geocoding.GetPlacemarksAsync(new Location(p.Latitude, p.Longitude))).FirstOrDefault();
    Street = $"{addrs.Thoroughfare} {addrs.SubThoroughfare}";
    City = $"{addrs.PostalCode} {addrs.Locality}";
    Country = addrs.CountryName;
    // ...
}

But now you have it, going from Coordinates to an address and back the other way. Easy right? Well, there is still one more thing; the default coordinates of the map point to Rome. And I get it. Rome has a lot to offer, but if your user is not currently in Rome, it might be quite a bit of pinching and swiping until the map will be useful. So long story short, why not use the user's position?

Marco? Polo!

Getting the users position is a piece of cake again, thanks to Xamarin Essentials. That is once you have required all the permissions as described in the link. With just a few lines we can get the address and set the position:

try
{
    var location = await Geolocation.GetLastKnownLocationAsync()
	var position = new Position(location.Latitude, location.Longitude);
	Position = position;
	// ... set address and stuff
}
// many exception handlers according to docs here


The user will get prompted the first time this code runs if they want to share their position. Just as a side note if you do not want to smother the user right of the batt with permission dialogues, you might want to put some thought into when this code should first be run.

So when the view appears, we can try to get the users position and tell the map control what part of the world to display:

Geolocation.GetLastKnownLocationAsync()
            .ToObservable()
            .Catch(Observable.Return(new Location()))
            .SubscribeOn(RxApp.MainThreadScheduler)
            .Subscribe(async location => { 
                var position = new Position(location.Latitude, location.Longitude);
                Position = position;
                await SetAddress(position);
            });

I hope you could see that it does not take much to go from tiresome form to a "fun" maps control.

GifRecordingSmall

And all by using standard libraries offered to you by Microsoft. You can find the complete sample on GitHub.

HTH

Post comment