inSmartBank

B/43を運営する株式会社スマートバンクのメンバーによるブログです

iOSのアイコン変更機能をリリースしました

こんにちは!スマートバンクでアプリエンジニアをしているkanekoです。 先日リリースしたB/43のバージョン12.6.0からiOS版限定でアプリアイコンの変更機能が利用できるようになりました!

今回はアプリアイコン変更機能について、実装方法やリリースまでの流れについて紹介します。

アイコン変更実装

iOSでのアイコン変更機能はiOS 10.3から利用可能な UIApplication.setAlternateIconNameを利用しています。以下で具体的な実装の手順を紹介します。

Appleの公式ドキュメント

変更対象のアイコンをプロジェクトに追加

変更対象のアイコンをApp Iconとして Assets に追加します。ImageSetではなくAppIconとして追加しないとアイコン設定時にエラーが発生するので注意が必要です。

Build Settingsに差し替え対象にするリソース名を追加

上で追加したAsset名をBuild Settingsの Asset Catalog Compiler > Alternate App Icon Setsで指定します。

アプリ上で選択されたアイコンに変更する処理を実装

Build Settingsに登録後は選択されたアイコンへ切り替える処理を実装します。 切り替えは UIApplication.setAlternateIconNameでBuild Settingsに追加したAsset名文字列(デフォルトアイコン時はnil)に指定することで可能になります。

特定のアイコン(ミントアイコン)を設定する際

UIApplication.shared.setAlternateIconName("AppIconMint")

デフォルトアイコンに戻す際

UIApplication.shared.setAlternateIconName(nil)

setAlternateIconName(_:completionHandler:) | Apple Developer Documentation

具体的な実装例

これだけでは味気ないので簡易なサンプルでアイコン変更画面のB/43での実装例をご紹介します。 以下のようなB/43のような簡易的なアイコン変更画面を実装しています。

作成するサンプルアプリ
まずは変更可能なアプリアイコンを表すenumを定義します。 ここではアイコン変更のために必要な iconNameを定義し上のステップでBuild Settingsに追加した文字列を返すようにしています。また、アイコン選択画面で変更後のアイコン画像のプレビュー・アイコン名というUI表示に必要なプロパティもここで定義しています。

enum AppIcon: String, CaseIterable, Identifiable {
    case `default` = "AppIcon"
    case mint = "AppIconMint"
    case sky = "AppIconSky"

    var id: String {
        rawValue
    }
    // setAlternateIconNameで指定する文字列
    var iconName: String? {
        return switch self {
        case .default: // 通常のアイコンに戻す際はnilを指定する
            nil
        case .mint, .sky: // その他アイコンはBuild Settingsで追加した文字列を指定する
            rawValue
        }
    }
    // リストで表示する文字列
    var description: String {
        return switch self {
        case .default:
            "デフォルト"
        case .mint:
            "ミント"
        case .sky:
            "スカイ"
        }
    }
    // リストで表示する画像リソース名
    var imageName: String {
        return rawValue
    }
}

次に定義したアプリアイコンを変更するView側の実装をしてみます。 設定済みのアイコン名は UIApplication.shared.alternateIconNameで取得可能です。これを利用して現在設定中のアイコン情報を保持する selectedIconを定義します。

struct IconSelectedView: View {
    @State var selectedIcon: AppIcon

    init() {
        if let alternateIconName = UIApplication.shared.alternateIconName, let appIcon = AppIcon(rawValue: alternateIconName) {
            selectedIcon = appIcon
        } else {
            selectedIcon = .default
        }
    }
        // ...
}

次にアイコンの更新処理を実装します。 先に書いた通りUIApplication.shared.setAlternateIconNameを使ってアイコンの更新をしますが、このメソッドを呼び出すとアイコンを変更した旨のOSダイアログが表示されます。これは呼び出し元のアプリ側の実装では非表示に出来ないうえ、現在設定中のアイコンを再選択した際も表示されてしまいます。そのため、現在設定中のアイコンと選択したアイコンの比較後に必要に応じてアイコンの更新を行うようにしています。

アイコン変更後のダイアログ
また、 setAlternateIconNameで発生する例外は設定したアイコンのリソースが存在しない際など限定的なものしか確認出来なかったため選択状態のリセットのみを実装しています。

struct IconSelectedView: View {

        // ...

    func updateAppIcon(appIcon: AppIcon) {
        // 選択したアイコンが設定済みの際にOSダイアログを表示させないようにアイコンの比較
        if selectedIcon == appIcon {
            return
        }

        let previousIcon = selectedIcon
        selectedIcon = appIcon

        Task { @MainActor in
            do {
                try await UIApplication.shared.setAlternateIconName(appIcon.iconName)
            } catch {
                self.selectedIcon = previousIcon
            }
        }
    }
}

最終的なViewの実装は以下のようになります。

struct IconSelectedView: View {
    @State var selectedIcon: AppIcon

    init() {
        if let alternateIconName = UIApplication.shared.alternateIconName, let appIcon = AppIcon(rawValue: alternateIconName) {
            selectedIcon = appIcon
        } else {
            selectedIcon = .default
        }
    }

    var body: some View {
        ScrollView {
            VStack {
                ForEach(AppIcon.allCases) { appIcon in
                    Button(action: {
                        updateAppIcon(appIcon: appIcon)
                    }, label: {
                        HStack(content: {
                            Image(uiImage: UIImage(named: appIcon.imageName)!)
                                .resizable()
                                .frame(width: 48, height: 48)
                            Text(appIcon.description)
                            Spacer()

                            if selectedIcon == appIcon {
                                Image(systemName: "checkmark")
                            }
                        })
                    })
                }
            }
        }
        .padding()
    }

    func updateAppIcon(appIcon: AppIcon) {
        if selectedIcon == appIcon {
            return
        }

        let previousIcon = selectedIcon
        selectedIcon = appIcon

        Task { @MainActor in
            do {
                try await UIApplication.shared.setAlternateIconName(appIcon.iconName)
            } catch {
                self.selectedIcon = previousIcon
            }
        }
    }
}

アイコン変更の実装裏話

きっかけ

今回のアイコン変更機能の実装ですが、アプリチームの自由研究の一環で企画がスタートしました。

自由研究とは(エンジニア自由研究活動の発表会レポートより)

自由研究部は、その名前の通りエンジニアがテーマを選んで、そのテーマにそった研究に取り組む部活動のことです。 スマートバンクのエンジニア陣は各プロジェクトチームにて、業務を行うことが多いので、必然的に業務時間のほとんどは自分が担当するプロジェクトに関する仕事がメインになります。 そういった開発を進める中で、B/43を改善するアイディアの実現や普段着手できていない技術的なTryをしてみたいという要望が度々あり、また他メンバーとオフラインでコラボレーションして開発する機会が減っていたことから、そういったものを研究テーマとして扱い、エンジニアがワイワイ集まりながら取り組む機会を作ろうと試験的に始まったのが自由研究活動です。

過去の自由研究での発表結果は以下で確認できます。

blog.smartbank.co.jp blog.smartbank.co.jp

アイコン種類について

当初の企画ではB/43で発行可能なカード種別に紐づいたアイコンに変更可能にする予定でした。

初期構想のアイコン
しかし社内にプロトタイプ版を配布したところデザインチームからアイコンを追加しても問題ないか?との打診が…!! そこからデザインチームにアイコンの案だしと社内アンケートを主導していただき今のアイコンの一覧が決定しました。
アイコン総選挙の様子

アイコン候補の中でもひときわ異彩を放つ犬アイコンは社内の公募で決定しました。

犬アイコンが決まった瞬間

リリース後

実装開始当初は知る人ぞ知る機能になれば良いなと思って実装をしていましたが、多くのメンバーの協力のおかげでアイコンのクオリティも上がり多くの方に認知いただける機能になりました。 まだ未設定の方がいらっしゃったらこの機会に是非使ってみていただけると幸いです。

参考までに現時点でのアイコン設定数ランキングも掲載して置きます。

アイコン設定数ランキング

最後に

以上がiOSにおけるアイコン変更機能の実装方法とリリースまでの歩みでした。

実装内容的にそこまで難しい内容では無いと思いますが、社内の雰囲気含め参考になったら幸いです。

最後に、スマートバンクでは一緒に B/43 を作り上げていくメンバーを募集しています!カジュアル面談も受け付けていますので、お気軽にご応募ください !!

smartbank.co.jp

We create the new normal of easy budgeting, easy banking, and easy living.
In this blog, engineers, product managers, designers, business development, legal, CS, and other members will share their insights.