Customizing .NET MAUI Map’s pin Z-Order, Rotation, and Anchor point – Android

This post will build upon the excellent tutorial on customizing map pins in .NET MAUI by Vladislav Antonyuk, and focus on adding these specific adjustments for Android.

  • Z-order: This determines the drawing order of the pins. Pins with higher Z-order values appear on top of those with lower values.
  • Rotation: Controls the rotation angle of the pin in degrees.
  • Anchor: Defines the point on the pin image that corresponds to the pin’s location on the map. The anchor is defined in terms of a normalized coordinate system where (0, 0) is the top-left corner and (1, 1) is the bottom-right corner of the image.

Define the Anchor Points enum, get a method to return anchor floats

We start by defining an enum to represent the different anchor points. A helper method, GetAnchorPoint, will return the appropriate anchor coordinates based on the chosen enum value.

public enum AnchorPointType
{
    TopLeft,
    TopCenter,
    TopRight,
    Center,
    BottomLeft,
    BottomCenter,
    BottomRight,
    LeftCenter,
    RightCenter
}

This method returns the normalized coordinates for each anchor point, which can then be applied to the pin’s icon.

private static (float, float) GetAnchorPoint(AnchorPointType anchor)
{
    return anchor switch
    {
        AnchorPointType.TopLeft => (0.0f, 0.0f),
        AnchorPointType.TopCenter => (0.5f, 0.0f),
        AnchorPointType.TopRight => (1.0f, 0.0f),
        AnchorPointType.Center => (0.5f, 0.5f),
        AnchorPointType.BottomLeft => (0.0f, 1.0f),
        AnchorPointType.BottomCenter => (0.5f, 1.0f),
        AnchorPointType.BottomRight => (1.0f, 1.0f),
        AnchorPointType.LeftCenter => (0.0f, 0.5f),
        AnchorPointType.RightCenter => (1.0f, 0.5f),
        _ => (0.5f, 0.5f)  // Default to center if undefined
    };
}

Modifications to the Custom Map Handler

Now, let’s focus on the custom map handler, where you’ll need to modify how each pin is added to the map. Following the setup from Vladislav’s tutorial, you only need to adjust the following parts in the OnMapReady method for Android:

cp.ImageSource.LoadImage(MauiContext, result =>
{
    if (result?.Value is BitmapDrawable bitmapDrawable)
    {
        var originalBitmap = bitmapDrawable.Bitmap;

        // Set the custom icon for the pin
        markerOption.SetIcon(BitmapDescriptorFactory.FromBitmap(originalBitmap));

        // Set the rotation of the pin
        markerOption.SetRotation((float)cp.Rotation);

        // Set the anchor of the pin based on the chosen AnchorPointType
        var anchor = GetAnchorPoint(cp.Anchor);
        markerOption.Anchor(anchor.Item1, anchor.Item2);

        // Set the Z-order to bring this pin to the top
        markerOption.InvokeZIndex(cp.ZOrder);
    }

    AddMarker(Map, pin, Markers, markerOption);
});

Disabling Map Zoom Controls and Other Buttons in .NET MAUI Android

In this post, we’ll guide you through the process of disabling these UI elements (zoom controls, compass, and location buttons) in your Android map implementation using a custom map handler. This approach gives you more control over the user experience and map functionality in your mobile app.

Creating a Custom Map Handler

To disable these controls, we need to customize how the map is rendered on Android. This involves creating a custom map handler that intercepts the way the map is displayed and adjusts its settings.

A detailed tutorial for creating custom map handlers can be found in this great guide by Vladislav Antonyuk. We will extend that concept here.

First, we need to implement a MapCallbackHandler that disables specific controls when the map is ready. This is done in the OnMapReady method, which is triggered when the map is fully loaded and ready for interaction.

using Android.Gms.Maps;
using Android.Gms.Maps.Model;
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Controls.Compatibility.Maps.Android;

class MapCallbackHandler : Java.Lang.Object, IOnMapReadyCallback
{
    private readonly IMapHandler mapHandler;

    public MapCallbackHandler(IMapHandler mapHandler)
    {
        this.mapHandler = mapHandler;
    }

    public void OnMapReady(GoogleMap googleMap)
    {
        // Update map with any pins or map state changes
        mapHandler.UpdateValue(nameof(IMap.Pins));
        
        // Disable zoom controls
        googleMap.UiSettings.ZoomControlsEnabled = false;
        
        // Disable the "My Location" button
        googleMap.UiSettings.MyLocationButtonEnabled = false;
        
        // Disable the compass
        googleMap.UiSettings.CompassEnabled = false;

        // Additional settings can be adjusted here, such as disabling tilt or gestures.
    }
}

In the OnMapReady method, we access the googleMap.UiSettings property, which contains several settings that control the map’s UI. In our example, we set the following to false:

  • ZoomControlsEnabled: Disables the zoom buttons.
  • MyLocationButtonEnabled: Removes the My Location button that appears when location services are enabled.
  • CompassEnabled: Hides the compass that appears when the user rotates the map.

You can also adjust other settings here, such as disabling tilt gestures or zoom gestures if needed.

Using Different Entitlements for Debug and Release Modes in .NET MAUI – iOS

When developing a mobile app using .NET MAUI, particularly for iOS, it’s essential to configure your application differently for debug and release modes. One of these differences is the APS-environment setting, which dictates how your app communicates with Apple Push Notification services (APNs) during development and production.

What is Entitlements.plist?

The Entitlements.plist is a property list (plist) file that defines various capabilities or entitlements for your app. Entitlements are special permissions that allow your app to use certain services provided by iOS, such as iCloud, In-App Purchases, or push notifications.

For push notifications, the Entitlements.plist file contains the APS-environment key, which indicates to Apple whether your app is in development or production mode. Based on this, the app uses either the sandbox or production APNs.

What is APS-environment?

The APS-environment (Apple Push Services environment) is an entitlement used to specify the environment for push notifications. This entitlement informs Apple’s servers whether the app is running in a development environment or in production, determining which server to send the notifications through:

  • Development APS-environment: Used for testing push notifications during the app’s development phase. These notifications go through Apple’s sandbox APNs server.
  • Production APS-environment: Used for apps that have been published and distributed through the App Store. Notifications go through Apple’s production APNs server.

This configuration helps separate testing from live user interactions and avoids accidental notification delivery to users during testing.

Configuring Different APS-environments for Debug and Release

To configure different environments for Debug and Release modes in your .NET MAUI project, you can modify your .csproj file as follows:

<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0-ios|AnyCPU'">
  <CodesignEntitlements>Platforms\iOS\Entitlements.plist</CodesignEntitlements>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0-ios|AnyCPU'">
  <CodesignEntitlements>Platforms\iOS\Entitlements-Production.plist</CodesignEntitlements>
</PropertyGroup>

It’s important to ensure that both Entitlements.plist and Entitlements-Production.plist files are not included in the build by accident. This can be achieved by setting their Build Action to None:

  1. Right-click on each file (Entitlements.plist and Entitlements-Production.plist) in Visual Studio.
  2. Select Properties.
  3. Set the Build Action to None.

This step ensures that the files are correctly associated with your app for code-signing purposes but are not compiled into the app bundle unnecessarily.

Azure Pipelines error after macOS / iOS update when building iOS apps with .NET MAUI

If you are facing problems after a new version of macOS, or iOS have been released, stay tuned.

In this example, I’m having issues with the new update of iOS 18 and macOS Sequioa. Some days or weeks are needed from Microsoft side to have a vmPool in the cloud to build your iOS apps with the latest SDKs, so theres a possibility that you might encounter this problem on some fresher updates aswell.

So how the error looks like?

ILLINK : error MT2362: The linker step 'ClassHandleRewriter' failed during processing: One or more errors occurred. (The type 'MapKit.MKSelectionAccessory' (used as a return type in MapKit.MKMapView/_MKMapViewDelegate.GetSelectionAccessory) is not available in iOS 17.2 (it was introduced in iOS 18.0). Please build with a newer iOS SDK (usually done by using the most recent version of Xcode). 
  		) (The type 'UIKit.UITextFormattingViewController' (used as a parameter in UIKit.UITextView/_UITextViewDelegate.DidBeginFormatting) is not available in iOS 17.2 (it was introduced in iOS 18.0). Please build with a newer iOS SDK (usually done by using the most recent version of Xcode).
  		) (The type 'UIKit.UITextFormattingViewController' (used as a parameter in UIKit.UITextView/_UITextViewDelegate.DidEndFormatting) is not available in iOS 17.2 (it was introduced in iOS 18.0). Please build with a newer iOS SDK (usually done by using the most recent version of Xcode).
  		) (The type 'UIKit.UITextFormattingViewController' (used as a parameter in UIKit.UITextView/_UITextViewDelegate.WillBeginFormatting) is not available in iOS 17.2 (it was introduced in iOS 18.0). Please build with a newer iOS SDK (usually done by using the most recent version of Xcode).
  		) (The type 'UIKit.UITextFormattingViewController' (used as a parameter in UIKit.UITextView/_UITextViewDelegate.WillEndFormatting) is not available in iOS 17.2 (it was introduced in iOS 18.0). Please build with a newer iOS SDK (usually done by using the most recent version of Xcode).
  		) (The type 'UIKit.UITab' (used as a parameter in UIKit.UITabBarController/_UITabBarControllerDelegate.AcceptItemsFromDropSession) is not available in iOS 17.2 (it was introduced in iOS 18.0). Please build with a newer iOS SDK (usually done by using the most recent version of Xcode).
  		) (The type 'UIKit.UITab' (used as a parameter in UIKit.UITabBarController/_UITabBarControllerDelegate.DidSelectTab) is not available in iOS 17.2 (it was introduced in iOS 18.0). Please build with a newer iOS SDK (usually done by using the most recent version of Xcode).
  		) (The type 'UIKit.UITab' (used as a parameter in UIKit.UITabBarController/_UITabBarControllerDelegate.DidSelectTab) is not available in iOS 17.2 (it was introduced in iOS 18.0). Please build with a newer iOS SDK (usually done by using the most recent version of Xcode).
  		) (The type 'UIKit.UITabGroup' (used as a parameter in UIKit.UITabBarController/_UITabBarControllerDelegate.DisplayOrderDidChangeForGroup) is not available in iOS 17.2 (it was introduced in iOS 18.0). Please build with a newer iOS SDK (usually done by using the most recent version of Xcode).
  		) (The type 'UIKit.UITab' (used as a parameter in UIKit.UITabBarController/_UITabBarControllerDelegate.GetDisplayedViewControllers) is not available in iOS 17.2 (it was introduced in iOS 18.0). Please build with a newer iOS SDK (usually done by using the most recent version of Xcode).
  		) (The type 'UIKit.UITab' (used as a parameter in UIKit.UITabBarController/_UITabBarControllerDelegate.GetOperationForAcceptingItemsFromDropSession) is not available in iOS 17.2 (it was introduced in iOS 18.0). Please build with a newer iOS SDK (usually done by using the most recent version of Xcode).
  		) (The type 'UIKit.UITab' (used as a parameter in UIKit.UITabBarController/_UITabBarControllerDelegate.ShouldSelectTab) is not available in iOS 17.2 (it was introduced in iOS 18.0). Please build with a newer iOS SDK (usually done by using the most recent version of Xcode).
  		) (The type 'UIKit.UITab[]' (used as a parameter in UIKit.UITabBarController/_UITabBarControllerDelegate.VisibilityDidChangeForTabs) is not available in iOS 17.2 (it was introduced in iOS 18.0). Please build with a newer iOS SDK (usually done by using the most recent version of Xcode).
  		)

Solution after the proper vmImages are released by Microsoft:

To resolve this issue, you need to update the macOS image used in your Azure Pipelines to the latest version. This ensures that the build environment uses the most recent version of Xcode, which includes the necessary iOS SDKs.

Here’s how you can update your Azure Pipelines configuration:

  • Open your Azure Pipelines YAML file: Locate the YAML file that defines your pipeline configuration.
  • Update the vmImage: Change the vmImage to macOS-latest to ensure that the latest macOS version is used, which includes the most recent Xcode and iOS SDKs.
pool:
  vmImage: 'macOS-latest'
  • Save and commit the changes: Save the updated YAML file and commit the changes to your repository.
  • Run the pipeline: Trigger a new build in Azure Pipelines to verify that the issue is resolved.

By updating the vmImage to macOS-latest, you ensure that your build environment is using the latest tools and SDKs, which should resolve the linker errors related to unavailable types.

Solution until the proper vmImages are not present:

Modify your pipeline, and don’t forget to add comments on the modified pipeline!

  • Open your Azure Pipelines YAML file: Locate the YAML file that defines your pipeline configuration.
  • Update the YAML file:
pool: 
     vmImage: 'macOS-14'
# András @ 24.10.04: Explicit version needed until macos15 vmimage is being released. Use latest when possible
    steps:
      - task: CmdLine@2
        displayName: 'Select XCode 16 explicitly'
        inputs:
          script: 'sudo xcode-select -s /Applications/Xcode_16.app'
# András @ 24.10.04: Explicit version code needed for XCode. Remove this task when possible

      - task: CmdLine@2
        displayName: 'Install MAUI Workload'
        inputs:
          script: 'dotnet workload install maui --version 8.0.402'
# András @ 24.10.04: Explicit version needed until macos15 vmimage is being released. Use no --version tag if possible
  • Save and commit the changes: Save the updated YAML file and commit the changes to your repository.
  • Run the pipeline: Trigger a new build in Azure Pipelines to verify that the issue is resolved.

.NET MAUI Android: OverrideBackButtonPress not being hit.

You might encounter a scenario where the OverrideBackButtonPress method in your Page is not being triggered on Android devices. This can be a frustrating issue, but there’s a straightforward solution that involves modifying your AndroidManifest.xml file.

The predictive back gesture feature in Android can indeed affect how back button presses are handled in your application. Learn more at: https://developer.android.com/guide/navigation/custom-back/predictive-back-gesture

Predictive Back Gesture

Android’s predictive back gesture allows users to preview the destination or action that will occur when they complete a back gesture. In .NET MAUI, the OverrideBackButtonPress method allows you to handle the back button press event in your application. However, if this method is not being called, it could be due to a specific setting in your AndroidManifest.xml file.

Disabling Predictive Back Gesture

To ensure your custom back button handling works as expected, you need to disable the predictive back gesture by setting the android:enableOnBackInvokedCallback attribute to false in your AndroidManifest.xml file. This prevents the system from intercepting the back button press and allows your application to handle it.

<application
    android:label="YourAppName"
    android:icon="@mipmap/ic_launcher"
    android:enableOnBackInvokedCallback="false">
    <!-- Other settings -->
</application>