Universal AppLinks on iOS


I was lucky to be tasked to get Universal AppLinks working, for a cool feature in the TrackMan Golf App I work on at work. I thought this would be super easy to do. But, oh how naive and wrong I was.

So in essence, the App is supposed to open if I hit a URL with a specific pattern for one of our domains. Something like: https://trackman.com/pin/123456. Where the numbers can differ.

I’ve done this before with a custom scheme like trackman://pin/123456, which is just a simple entry in the Info.plist for your App. However, for Universal AppLinks, there are many more moving parts involved.

1. Associate your App(s) to a specific domain

I guess, in order to not be able to open arbitrary URLs in your App. Apple, requires you to host a file called apple-app-site-association on your Web Server. This file should not have any file extensions. Make sure you also serve it with the Content-Type: application/json. Also, there should be no redirects to get to this URL.

This file should be in either the root or in a .well-known folder. For instance:

  • https://trackman.com/apple-app-site-association
  • https://trackman.com/.well-known/apple-app-site-association

Apple will check the .well-known folder first, then fall back to the root.

This check happens when you install the App on your device. There is a process called swcd that will run and check these associasions on your device during install. If any of this stuff fails, this swcd process is what you look for in the device log. The errors will look something like this.

Request for '<private>' for task AASA-80AD262A-3EF6-42A2-B992-AC97234187647 { domain: *.tr....com, bytes: 0, route: cdn } denied because the CDN told us to stop with HTTP Status...

You will need to deduct yourself which domain it relates to, because the console entries redact some of the values. However, it will tell you what is wrong. Some common issues are:

  • SSL certificate doesn’t match the domain entry you’ve registered
  • Apple CDN cannot access the file
  • The JSON is in a wrong format

Yes, you read it right. From iOS 14 and up, Apple will hit the file through their CDN. So you have to make sure their CDN can reach the file.

The contents of this JSON file will look as follows. Also refer to Listing 6-1 in these Apple docs. for more details of the format.

    "applinks": {
        "apps": [],
        "details": [
                "appID": "<app id prefix>.<bundle id>",
                "paths": [ "/pin/*"]

The JSON file format that they suggest in the Supporting Associated Domains documentation, did not work for me! If something doesn’t work, try swiching between the two. Unfortunately Apple does not provide a JSON-Schema for this, so it is not easy to validate if you entered stuff correctly, except that you are returning valid JSON.

The App id prefix and bundle name can be located in the developer portal where you have created your App in the Identifiers section. The App Id will be the same as your Team ID usually. Bundle id, is the Id of your App that you have registered in there. So appID that goes into the JSON file will look something like: 2V9BB354QZ.my.awesome.app.

The paths is an array of paths. So anything that goes after your domain. You can use wildcards like:

  • * will match any substring
  • ? will match a single character


  • /tamarin will match a specific path
  • /cats/* will match everything in the “cats” section of your website
  • /cats/archives/202? will match everything in “cats/archives” and 2020-2029

You can combine the wildcard characters. The paths array can have multiple, comma delmited paths for a App Id.

To exclude a path you can write NOT /cats/*.

2. Enabling the Associated Domain entitlement in provisioning profiles

Go to Certificates, Identifiers & Profiles in the Apple Developer Portal. Here you will need to enable the “Associated Domains” entitlement for your App.

In the list of Identifiers find your App and Edit it. Enable the “Associated Domains” entitlement Screenshot of Associated Domains option in Developer Portal

Now go to any Provisioning profiles you have an regenerate them. You might be using Fastlane or something like that. Read the docs for your tool to download or regnerate provisioning profiles.

After regenerating the Provisioning profiles, you might need to update any CI pipeline and machines that you build on with these new profiles.

3. Adding Associated Domains to your Entitlements.plist files

If you don’t have a Entitlements.plist file, it is now time to create one. Microsoft provides good docs for you to read more about how to create one. You will need to add all the domains you wish your Apps to open. These of course need to match the domains you added in the first step in the apple-app-site-association file. This will look something like:


You can also use wildcards here:


This will match any sub-domain, but not include the raw ostebaronen.dk domain. If you can avoid it, I recommend not to use the wildcard here. I had issues with the Apple CDN not being able to process the apple-app-site-association file, since it seems like it expects a wildcard entry in the SSL certificate too. I did not have that. If you are going to use it, make sure to test this thoroughly.

4. Extending AppDelegate.cs to handle the URL requests to your App

Luckily it seems like how our App is triggered is that first it launches the App fully. So FinishedLaunching is allowed to complete. Then ContinueUserActivity gets triggered with the URL. For good measures I also added an override for UserActivityUpdated. So in your AppDelegate.cs override these two methods.

public override bool ContinueUserActivity(UIApplication application, NSUserActivity userActivity, UIApplicationRestorationHandler completionHandler)
    return true;

public override void UserActivityUpdated(UIApplication application, NSUserActivity userActivity)

To get the URL from the NSUserActivity I got inspired from the AppDelegate.cs that Xamarin.Forms provides.

The code looks like:

private void CheckForAppLink(NSUserActivity userActivity)
    var strLink = string.Empty;
    if (userActivity.ActivityType == "NSUserActivityTypeBrowsingWeb")
        strLink = userActivity.WebPageUrl.AbsoluteString;
    else if (userActivity.UserInfo.ContainsKey(new NSString("link")))
        strLink = userActivity.UserInfo[new NSString("link")].ToString();

    // do something clever with your URL here

5. Debugging all of this

This part is a huge pain. First of all, it appears to me, when you deploy an Application using Xcode or Visual Studio, the swcd process doesn’t run and check the apple-app-site-association file. This means when you hit your URL in Safari or through a QR code, it will not suggest to open your App. I had to grab builds from my CI pipeline for this to work 😢.

But things to check:

  • apple-app-site-association is reachable
  • the URL you are hitting is not returning 404 (although it might actually still work)
  • entries in your Entitlements.plist match the association json file
  • if you have multiple Entitlements for different configurations, make sure you are using the correct one
  • check the console output on your device
    • Xcode -> Window -> Devices & Simulators -> Devices -> Select device -> Open Console
    • Click Errors and Faults
    • add swcd in the search field

6. Bonus: Handling custom schemes

If you also need to handle custom URL schemes. The only thing you need to do is to register the schemes in your Info.plist file:


Then override OpenUrl in your AppDelegate.cs file:

public override bool OpenUrl(UIApplication app, NSUrl url, NSDictionary options)
    if (url.ToString().Contains("/my-pattern"))
        return true;

    return base.OpenUrl(app, url, options);

New WiFi API in Android 11


Android 10 messed up royally with removing the API to add networks and connect to it on a users device. The API was removed and as an alternative they gave us:

  1. A suggestion API, which shows a low priority notification, suggesting to connect to a given network. This notification would need to be swiped down a couple of times to reveal the YES/NO options. Then the device might choose not to connect to it anyways.
  2. A way to connect to a local network, not providing any Internet connectivity, through a System dialog appearing in the App. This dialog took forever to find your network. However, this method was not very useful for anyone as no Internet on this connection.

The last alternative, but technically not a part of the API for connecting to WiFi, would be to show a settings panel, where the user could manually enter credentials for a WiFi network in the list.

New Android 11 API

Note: this code will throw a ActivityNotFoundException if run on older Android versions. Make sure to add some version checking.

I guess the folks at Google got a bit backlash or they somehow changed their mind. However, they’ve now added an Intent you can fire to add a Network on the device.

The code for this looks something like this:

var intent = new Intent(
var bundle = new Bundle();
    new List<IParcelable>
        new WifiNetworkSuggestion.Builder()


StartActivityForResult(intent, AddWifiSettingsRequestCode);

Note: AddWifiSettingsRequestCode is just an Integer you define.

Then in OnActivityResult you can figure out whether any network in the list you provided was added with:

if (requestCode == AddWifiSettingsRequestCode)
    if (data != null && data.HasExtra(
        var extras =
                ?.Select(i => i.IntValue()).ToArray() ?? new int[0];

        if (extras.Length > 0)
            var ok = extras
                .All(r => r == Result.Ok);
            // if ok is true, BINGO!

The extras will return a list of Result which indicate whether they were added if it is Result.OK or not if it is Result.Cancel.

GetResultFromCode, simply parses the integers returned and turns them into a Result:

private static Result GetResultFromCode(int code) =>
    code switch
        0 => Result.Ok, // newly added
        2 => Result.Ok, // wifi already there
        _ => Result.Canceled

Running this code will show you a dialog looking something like this

Screenshot of Save this network dialog

Note: This code works and runs fine on a Google Pixel 3a XL running latest Android 11. However, on my OnePlus 8 running OP8_O2_BETA_3 opening the intent fails, because of OnePlus’s Settings App does not implement the AppCompat theme, crashing. I’ve reported this issue to OnePlus but never heard back from them.

You can check out the code from my Android 11 WiFi Repository on GitHub and have a go testing it yourself.

Old MvvmCross versions and Android 10 Play Store requirement


If you are looking for samples scroll down to the bottom of this blog post

I have gotten questions from multiple people, about versions of MvvmCross prior to version 6.4.1. What they can do about being forced to target Android 10, API 29, or newer from November 2nd, when Google stops accepting updates to Apps targeting lower API levels.

MvvmCross 6.4.1 introduced some changes to MvxLayoutInflater that are needed in order for it to work with Android 10. However, these changes are for obvious reasons not part of previous releases.

Luckily MvvmCross is written in a way where you can replace parts of it through virtual methods and implementing certain interfaces. This can be used to retrofit old versions of MvvmCross with newer patched versions of, in this case, MvxLayoutInflater.

You simply have to follow these 3 simple steps!

  1. Go to the corner
  2. Curl up in a ball
  3. Cry 😭

Sorry, wait, no. Thats not it! Upgrade MvvmCross! But, if you really cannot upgrade then try these steps.

  1. Grab latest MvxLayoutInflater
  2. Create your own implementation of MvxContextWrapper which uses 1.
  3. Create your own MvxActivity derivatives that uses ContextWrapper from 2.
  4. Replace all MvxActivities with your own

OK, I lied that was 4 steps. You might also need a 5th step, where you target Android 10 in your App, otherwise all this work will be useless anyways…

Lets go through all steps.

1. Get latest MvxLayoutInflater

Grab latest MvxLayoutInflater and MvxLayoutInflaterCompat, change namespace to something more suitable in your App. Rename the class FixedLayoutInflater or whatever you prefer.

2. Create your own implementation of MvxContextWrapper

We have to supply our own MvxContextWrapper class, the code in there is super simple. It just tells Android which LayoutInflater to use. We want to use our FixedLayoutInflater in there.

using System;
using Android.Content;
using Android.Runtime;
using Android.Views;
using MvvmCross.Binding.BindingContext;
using Object = Java.Lang.Object;

namespace Awesome.App
    public class FixedContextWrapper : ContextWrapper
        private LayoutInflater _inflater;
        private readonly IMvxBindingContextOwner _bindingContextOwner;

        public static ContextWrapper Wrap(Context @base, IMvxBindingContextOwner bindingContextOwner)
            return new FixedContextWrapper(@base, bindingContextOwner);

        protected FixedContextWrapper(Context context, IMvxBindingContextOwner bindingContextOwner)
            : base(context)
            if (bindingContextOwner == null)
                throw new InvalidOperationException("Wrapper can only be set on IMvxBindingContextOwner");

            _bindingContextOwner = bindingContextOwner;

        public override Object GetSystemService(string name)
            if (string.Equals(name, LayoutInflaterService, StringComparison.InvariantCulture))
                return _inflater ??=
                    new FixedLayoutInflater(LayoutInflater.From(BaseContext), this, null, false);

            return base.GetSystemService(name);

3. Create your own MvxActivity derivate

Next step is to create your own MvxActivity derivate. Why? Because you need to supply that ContextWrapper we just created. If you were to simply inherit from MvxActivity and override OnAttachContext, which is where we supply the ContextWrapper, then it would still use the original MvxContextWrapper, since the only way to supply it is by calling base.OnAttachContext.

OK. So yank whatever MvxActivity type you are re-implementing. Here is a non-exhaustive list of types we had.

MvvmCross Version Type
4.4.0 MvxActivity
4.4.0 MvxAppCompatActivity
5.7.0 MvxActivity
5.7.0 MvxAppCompatActivity

The part to replace is the contents of OnAttachContext which should look something like:

protected override void AttachBaseContext(Context @base)
    if (this is IMvxAndroidSplashScreenActivity)
        // Do not attach our inflater to splash screens.
    base.AttachBaseContext(FixedContextWrapper.Wrap(@base, this));

With that done, now you just have to replace every inheritance from MvxActivity in your App with this implementation, except for any splash screens, not needed there.

You may have to think a bit here and modify the code to your needs, but these are the simplest steps I could come up with.

You can find sample projects with fixes implemented on my GitHub in the repository OldMvvmCross-Android10

MvvmCross Code Snippets


This blog post is a part of Louis Matos’s Xamarin Month, where this months topic is Code Snippets. For more information take a look at his blog and see the list of all the other auhtors who are participating. There will be a new post each day of the month, which is super cool!

Let me share some code snippets that I often use. All of these snippets will be available in my XamarinSnippets code repository on GitHub, for import in Visual Studio 2019, ReSharper, Rider and Visual Studio for Mac. Instructions provided in the repository Readme file.

When writing an Application using MvvmCross, or even with other frameworks, there are some pieces of code that you have to repeat again and again. As programmers we are usually a bit lazy and don’t want to type all that code over and over again. Fortunately, we can have code snippets ready to help us, a nice feature built into our IDE’s. Some of the snippets I use often are as follows.


I often have to create a property in my ViewModels which raise the PropertyChanged event in the MvvmCross flavor. For that I simply type mvxprop and press Tab, and magically I get the following code.

private int propertyName;
public int PropertyName
    get => propertyName;
    set => SetProperty(ref propertyName, value);

I have considered making some flavors for some common types that I use, such as string, bool, int. Not sure how much I would use them.


Creating commands is also something that I often do. However, I’ve gone away from using this pattern where I lazily initialize commands, some of you may find them useful. What I prefer instead is initialize them in the constructor of the ViewModel instead.

private MvxCommand _command;
public MvxCommand Command =>
    _command ??= new MvxCommand(DoCommand);

private void DoCommand()
    // do stuff

I have a couple of variants of this snippet, which creates different types of MvxCommand. mvxcomt for the generic MvxCommand<T>, mvxcomasync for the async version MvxAsyncCommand and similarly the generic version of that mvxcomtasync which creates a MvxAsyncCommand<T>.


Last but not least, a snippet for creating a binding set for binding views on both Android and iOS. I’ve taken the liberty to make the assumption that View and ViewModel names follow each other. So PeopleView will usually have a corresponding ViewModel PeopleViewModel. This ends up creating something like this:

using var set = this.CreateBindingSet<PeopleView, PeopleViewModel>();

Get all the snippets along with instructions in my XamarinSnippets repository on GitHub.

Do you have any cool related MvvmCross snippets to share? Put them in the comments or make a Pull Request on the repository.

Released a Color Picker Library