Xamarin - Alternate App Icons

Alternate app icons with Xamarin

UPDATE (15th of July 19): New and improved method for Android can be found here.

Your app’s icon is an important part of its personality and just like a cup of Guatemala Single Origin Coffee, with sticky nougat mouthfeel, one is never enough. Thankfully adding alternate icons to your apps is relatively easy on iOS and only moderately difficult on Android.

There is a GitHub link down below with all the sample code, as well as Microsoft Docs for iOS and a Stack Overflow discussion about Android.

Since I’m a fan of emoji we’re going to setup two icons for this demo; a chicken (the default 🐔) and a cactus (the alternate 🌵).

iOS

Add your alternate icons into your app’s Resource’s folder (not Assets.xcassets).

Adding alternate icon to iOS’s Resources folder

We need to register any alternate icons with iOS through info.plist. Add a new dictionary, CFBundleIcon, which contains another dictionary, CFBundleAlternateIcons, which contains yet another dictionary for each alternate icon set. For the sample below we have one icon set named cactus with one icon file (you can add multiple, but leave out the @2x/@3x extension).

<key>CFBundleIcons</key>
<dict>
    <key>CFBundleAlternateIcons</key>
    <dict>
        <key>cactus</key>
        <dict>
            <key>CFBundleIconFiles</key>
            <array>
                <string>icon_alt</string>
            </array>
            <key>UIPrerenderedIcon</key>
            <false/>
        </dict>
    </dict>
</dict>

At this point iOS makes changing icons easy with an API available through UIApplication.

void SetAlternateIconName(
    string alternateIconName,
    Action<Foundation.NSError> completionHandler);

The alternateIconName parameter corresponds to the icon set name we added to info.plist, in this case cactus, with null representing the default. The action completionHandler is run after the call completes with any error that occurred passed through.

In the sample I have created an event handler on the main Application that passes through an enum, AppIcon, defining which icon is selected.

public enum AppIcon
{
    Chicken,
    Cactus
}

public partial class App : Application
{
    public EventHandler<AppIcon> AppIconChanged;
    ...
}

In the AppDelegate I subscribe to the event and change the icon when requested.

public override bool FinishedLaunching(
    UIApplication app,
    NSDictionary options)
{
    global::Xamarin.Forms.Forms.Init();
    var altIconApp = new App();
    altIconApp.AppIconChanged += (sender, appIcon) =>
    {
        // null for default icon
        var iconName = 
            appIcon == AppIcon.Chicken
            ? null : appIcon.ToString().ToLower();
        UIApplication
            .SharedApplication
            .SetAlternateIconName(iconName, (err) => 
                {
                    Console.WriteLine("Set Alternate Icon: {0}", err);
                });
    };
    LoadApplication(altIconApp);
    return base.FinishedLaunching(app, options);
}

The final result.

Animation showing the icon changing on iOS

Android

Unlike iOS there is no official support for alternate icons in Android. The method I describe here is the most reliable I found thus far.

Similar to iOS we start by adding the alternate icon into the appropriate Drawable folders under Resources.

Adding alternate icon to Android’s Resources folder

Next we have to add an activity alias for the main launcher of our app. The launcher activity, much like it’s namesake, “launches” the app and is where the drawer icon is defined. Our app starts with a splash screen, SplashActivity, so we define an alias, SplashActivityAlias, in the AndroidManifest.xml with the alternate icon.

<application android:label="Alternate Icon">
    <activity-alias android:label="Alternate Icon"
        android:icon="@drawable/icon_alt"
        android:name="com.dgatto.alternateicon.SplashActivityAlias"
        android:targetActivity="com.dgatto.alternateicon.SplashActivity"
        android:enabled="false">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity-alias>
</application>

Xamarin Forms is loaded inside of the MainActivity which is started from SplashActivity once ready. We subscribe to the AppIconChanged event within MainActivity.

protected override void OnCreate(Bundle savedInstanceState)
{
    TabLayoutResource = Resource.Layout.Tabbar;
    ToolbarResource = Resource.Layout.Toolbar;
    base.OnCreate(savedInstanceState);
    global::Xamarin.Forms.Forms.Init(this, savedInstanceState);
    var altIconApp = new AlternateIcon.App();
    altIconApp.AppIconChanged += (sender, icon) =>
    {
        var splashActivity =
            new Android.Content.ComponentName(
                this,
                "com.dgatto.alternateicon.SplashActivity");
        var splashActivityAlias =
            new Android.Content.ComponentName(
                this,
                "com.dgatto.alternateicon.SplashActivityAlias");

        switch (icon)
        {
            case AppIcon.Chicken:
                PackageManager.SetComponentEnabledSetting(
                    splashActivity,
                    ComponentEnabledState.Enabled,
                    ComponentEnableOption.DontKillApp);
                PackageManager.SetComponentEnabledSetting(
                    splashActivityAlias,
                    ComponentEnabledState.Disabled,
                    ComponentEnableOption.DontKillApp);
                break;
            case AppIcon.Cactus:
                PackageManager.SetComponentEnabledSetting(
                    splashActivityAlias,
                    ComponentEnabledState.Enabled,
                    ComponentEnableOption.DontKillApp);
                PackageManager.SetComponentEnabledSetting(
                    splashActivity,
                    ComponentEnabledState.Disabled,
                    ComponentEnableOption.DontKillApp);
                break;
            default:
                break;
        }
        // Stop app getting killed by launching a new Activity
        var intent =
            new Android.Content.Intent(
                this,
                typeof(MainActivity));
        intent.AddFlags(Android.Content.ActivityFlags.ClearTop);
        intent.SetFlags(
            Android.Content.ActivityFlags.NewTask |
            Android.Content.ActivityFlags.ClearTask);
        Finish();
        StartActivity(intent);
    };
    LoadApplication(altIconApp);
}

The idea is to enable the launcher activity corresponding to the correct icon and disable all others. When a launcher activity is disabled it no longer appears inside the Android drawer. Unfortunately, even though we pass ComponentEnableOption.DontKillApp the app is still killed off after a few seconds. My workaround is to create an Intent to launch a new instance of MainActivity and call Finish on the current. This will reset your app’s state, so you may need to pass though a bundle in the intent and handle it in OnCreate.

The problems with this method are:

  1. Your app’s state is reset
  2. The icon is not updated instantly (you may notice the old icon in the drawer for a few seconds)
  3. The previous launcher activity is disabled, so if the user had a shortcut on their home screen it will disappear
  4. Xamarin cannot attach the debugger if any of the aliases are enabled (you’ll have to uninstall or change back to the default icon)

The final result.

Animation showing the icon changing on iOS

Conclusion

Changing your app’s icon in iOS is easy though the provided API, leaving that sweet sticky nougat mouthfeel. Android is a little more complicated and has a number of drawbacks, leaving a sour aftertaste that might turn you off; I’d be curious if anyone out in the ether has a better solution.

Resources

comments powered by Disqus