Dynamically alternate/change app icon

A simple example on how to dynamically alternate/change the app icon of your app

Thomas Asheim Smedmann
6 min readJul 9, 2023
Let your users dynamically alternate/change your app’s icon.

There is several examples of apps that let the user select the app icon their app installation should display on the home screen etc. One of my favourites is the GitHub App.

So why not give your own app users that little extra personalisation option, and let them be able to select their preferred app icon for your app!

If you want to get your hands dirty right away, check out this complete example app on how to dynamically change the app icon: https://github.com/thomsmed/ios-examples/tree/main/AlternateAppIcon.

Key takeaways

  • Use UIApplication.alternateIconName and UIApplication.setAlternateIconName(_:completionHandler:) to manage your app’s icon.
  • Additional app icon assets can be added to your app’s Asset catalog(s) as iOS App Icon assets (Add -> iOS -> iOS App Icon). Remember to check “Include all app icon assets” in your project file under your app target (General -> App Icons and Launch Screens).
  • Programmatically get the (asset) names of both your app’s primary app icon and any alternate app icons by inspecting your app’s Info.plist. Look for CFBundlePrimaryIcon and CFBundleAlternateIcons. Tip: To ease the work of any mapping from icon asset names to more user friendly names (when listing up alternate app icons), give your icon assets a user friendly name in your Asset catalog(s).

Preparations

UIApplication+Application

UIApplication provides the APIs necessary to dynamically alternate/change your app’s app icon. Even though the shared UIApplication instance is globally accessible via UIApplication.shared, we’ll extract these APIs behind our own Application protocol (mostly for dependency injection purposes).

import UIKit

protocol Application: AnyObject {
var alternateIconName: String? { get }
func setAlternateIconName(_ alternateIconName: String?, completionHandler: ((Error?) -> Void)?)
}

extension UIApplication: Application {}

For Xcode to also automatically bundle any alternate / additional App Icon asset, we’ll have to enable it in the Xcode project settings (Project -> App target -> General -> App Icons and Launch Screens).

Check “Include all app icon assets” to let Xcode automatically bundle any alternative / additional App Icon assets.

AppInfoProvider

The asset names of your app’s primary app icon and any alternate app icons can be found programmatically by inspecting your app’s Info.plist.

We’ll wrap the logic of fetching this information inside an AppInfoProvider protocol.

import Foundation

protocol AppInfoProvider: AnyObject {
var bundleDisplayName: String { get } // Your app's name.
var bundleVersion: String { get } // The current build number, e.g 137
var bundleShortVersionString: String { get } // The current app version, e.g 1.2.0.
var primaryAppIconName: String { get } // Name of the asset representing the primary app icon.
var alternateAppIconNames: [String] { get } // List of names of assets representing the alternate app icons.
}

final class DefaultAppInfoProvider: AppInfoProvider {
private func getValue<Value>(for key: String) -> Value {
guard
let value = Bundle.main.infoDictionary?[key] as? Value
else {
fatalError("Missing value for \(key) in Info.plist")
}
return value
}

private func getPrimaryAppIconName() -> String {
let appIconsDict: [String: [String: Any]] = getValue(for: "CFBundleIcons")

let primaryIconDict = appIconsDict["CFBundlePrimaryIcon"]

guard let primaryIconName = primaryIconDict?["CFBundleIconName"] as? String else {
fatalError("Missing primary icon name")
}

return primaryIconName
}

private func getAlternateAppIconNames() -> [String] {
let appIconsDict: [String: [String: Any]] = getValue(for: "CFBundleIcons")

let alternateIconsDict = appIconsDict["CFBundleAlternateIcons"] as? [String: [String: String]]

var alternateAppIconNames = [String]()

alternateIconsDict?.forEach { _, value in
if let alternateIconName = value["CFBundleIconName"] {
alternateAppIconNames.append(alternateIconName)
}
}

return alternateAppIconNames
}

lazy var bundleDisplayName: String = getValue(for: "CFBundleDisplayName")
lazy var bundleVersion: String = getValue(for: "CFBundleVersion")
lazy var bundleShortVersionString: String = getValue(for: "CFBundleShortVersionString")
lazy var primaryAppIconName: String = getPrimaryAppIconName()
lazy var alternateAppIconNames: [String] = getAlternateAppIconNames()
}

AppDependencies

At this point we have two “services” that handle the logic of fetching and changing app icon — Application and AppInfoProvider.

As a way of providing these “services” to the rest of our app, we’ll define an AppDependencies container to hold singleton instances of these.

import UIKit

protocol AppDependencies: AnyObject {
var application: Application { get }
var appInfoProvider: AppInfoProvider { get }
}

final class DefaultAppDependencies: AppDependencies {
let application: Application
let appInfoProvider: AppInfoProvider

init() {
application = UIApplication.shared
appInfoProvider = DefaultAppInfoProvider()
}
}

Our implementation of AppDependencies will it self be a singleton attached to our AppDelegate.

import UIKit

final class AppDelegate: NSObject, UIApplicationDelegate {

// Our shared AppDependencies instance.
private(set) lazy var appDependencies = DefaultAppDependencies()

func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil
) -> Bool {
return true
}
}

Since the APIs for managing alternate app icons are currently only accessible via UIKit’s UIApplication interface, we’ll have to implement a UIApplicationDelegate.

UIApplicationDelegate and SwiftUI

Since this example app will use SwiftUI, we’ll have to use the UIApplicationDelegateAdaptor property wrapper to get a hold on our AppDelegate instance.

This will also make sure our app is actually provided with the shared instance of UIApplication we need in order to dynamically change app icon (the necessary APIs are currently only accessible via UIKit’s UIApplication interface).

import SwiftUI

@main
struct MyApp: App {
@UIApplicationDelegateAdaptor var appDelegate: AppDelegate

var body: some Scene {
WindowGroup {
// We'll inject Application and AppInfoProvider via our ViewModel's initialiser.
AlternateAppIconsView(viewModel: .init(
application: appDelegate.appDependencies.application,
appInfoProvider: appDelegate.appDependencies.appInfoProvider
))
}
}
}

Finalise

Additional App Icon assets

To actually let the user be able to select their preferred app icon, we’ll have to provide some additional App Icon assets the user can select from.

Additional iOS App Icon assets can be added to your Asset catalog(s) via the menu in Xcode.
Give your App Icon assets user friendly names, so they are easy to list and select from.

List - and select from - available app icons

In this simple example SwiftUI app we’ll just have an AlternateAppIconsView where the user can scroll through and select which App Icon the app should use.

import SwiftUI

struct AlternateAppIconsView: View {
@StateObject var viewModel: ViewModel

var body: some View {
NavigationStack {
VStack {
ForEach(viewModel.icons, id: \.appIconName) { icon in
HStack {
Image(uiImage: UIImage(named: icon.appIconName)!)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 75, height: 75)
.cornerRadius(16)
.padding(8)

Text(icon.appIconName)
.padding(8)

Spacer()

if icon.selected {
Image(systemName: "checkmark.circle.fill")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 24, height: 24)
}
}
.onTapGesture {
viewModel.selectAppIcon(icon.appIconName)
}
.padding(.horizontal, 16)
}
Spacer()
}
.navigationTitle("App Icon")
}
}
}

Each row in the App Icon list will be represented by a simple AppIconCellModel struct.

extension AlternateAppIconsView {
struct AppIconCellModel {
let appIconName: String
let selected: Bool
}
}

And an associated ViewModel will hold data and logic for listing and selecting preferred app icon.

extension AlternateAppIconsView {
@MainActor final class ViewModel: ObservableObject {
private let application: Application
private let appInfoProvider: AppInfoProvider

private var currentlySelectedIconName: String

@Published var icons: [AppIconCellModel] = []

init(application: Application, appInfoProvider: AppInfoProvider) {
self.application = application
self.appInfoProvider = appInfoProvider

currentlySelectedIconName = application.alternateIconName ?? appInfoProvider.primaryAppIconName

rePopulateIcons()
}

private func rePopulateIcons() {
// List of all alternate app icon names.
var icons = appInfoProvider.alternateAppIconNames.map { appIconName in
AppIconCellModel(
appIconName: appIconName,
selected: appIconName == currentlySelectedIconName
)
}

// Append the name of our app's primary icon to the list as well.
icons.append(
AppIconCellModel(
appIconName: appInfoProvider.primaryAppIconName,
selected: appInfoProvider.primaryAppIconName == currentlySelectedIconName
)
)

// Sort the list of app icon names alphabetically,
// since the list of alternate app icon names are unordered by nature.
icons.sort(by: { l, r in l.appIconName > r.appIconName })

self.icons = icons
}

func selectAppIcon(_ appIconName: String) {
guard appIconName != currentlySelectedIconName else {
return
}

currentlySelectedIconName = appIconName

rePopulateIcons()

// Nil == no alternate icon selected (the system will then use the primary app icon).
let alternateIconName = appIconName == appInfoProvider.primaryAppIconName
? nil
: appIconName

application.setAlternateIconName(alternateIconName) { error in
if let error {
assertionFailure("Somehow failed to set alternate icon name. Error: \(error)")
}
}
}
}
}

That’s it!

There it is! A complete example on how to dynamically alternate/change your app’s app icon.

Again, the complete example code can be found on GitHub: https://github.com/thomsmed/ios-examples/tree/main/AlternateAppIcon.

Happy coding! 🙌

--

--