Money Forward Developers Blog

株式会社マネーフォワード公式開発者向けブログです。技術や開発手法、イベント登壇などを発信します。サービスに関するご質問は、各サービス窓口までご連絡ください。

20230215130734

マネーフォワード MEのControl Widget対応 〜フィーチャーのノミネート機能を添えて〜


この記事は、Money Forward Engineering Advent Calendar 2024 12月11日の投稿です。
12月10日は、とんがりさんでPanda CSSで作ったUIコンポーネントライブラリのスタイルが上書きされたり上書きされなかったりした話 でした。


こんにちは、モバイルエンジニアの高橋です。
マネーフォワード MEの開発を担当しています。

マネーフォワード MEでは、iOS 18から使用可能なコントロールウィジェットに対応しました。
また今回のアップデートに伴い、App Store Connectのフィーチャーのノミネート機能を活用し、幸いにもApp Storeのアプリタブでフィーチャーされました🎉

この記事では、コントロールウィジェットの対応とノミネート機能について紹介します。

1. コントロールウィジェットとは

コントロールウィジェットとは、アプリ側で提供するコントロールのことです。

コントロールとは?
システムのほかの領域からアプリの機能に素早くアクセスできるボタンやトグルのことです。
コントロールは、コントロールセンター、ロック画面、アクションボタンからアプリの機能に素早くアクセスすることができます。

参照: Human Interface Guidelines/Controls

iOS 18からは、アプリ側でコントロールを提供することで、コントロールセンターに追加できるようになりました。
コントロールセンターは、ロック画面からでもアクセス可能な特別な領域で、どの画面からもアクセスしやすい機能です。
そのため、今回のアップデートはユーザー体験に大きなインパクトを与えると考えています。

1-2. マネーフォワード MEで提供しているコントロールウィジェット

コントロールセンター ロックスクリーン

マネーフォワード MEでは、家計簿のメイン機能とよく使われる画面へ素早くアクセスできるように、以下の4種類のボタンをコントロールとして追加しました。

  • 入出金を入力
  • レシート撮影
  • ホーム画面を開く
  • 入出金画面を開く

コントロールセンターに配置できるコントロールのサイズは3種類あります。
そのため、それぞれのサイズ表示で問題がないか確認しておくことをおすすめします。

1-3. 実装例

Control Widgetからアクションが定義されたApp Intentを呼び出しています。
そのため、まず最初にApp Intentの実装が必要です。
コードを見ながら実装例を見てみましょう。

1-3-1. App Intent

struct ExampleIntent: AppIntent {
    static var title: LocalizedStringResource = "Example"
    static var openAppWhenRun: Bool = true
    
    @MainActor
    func perform() async throws -> some IntentResult {
        <特定の画面に遷移する処理>
        return .result()
    }
}

基本的には、AppIntentのインターフェースに準拠する形で実装をします。
ここで重要なポイントは、<特定の画面に遷移する処理>の部分です。
外部からアプリ内の特定の画面に遷移するために、Universal LinksやCustom URL Schemeなど活用することが考えられます。
もしアプリがUniversal Linksに対応しており、かつiOS 18.1 以上の環境であれば、OpenURLIntentを使用することが可能です。

@MainActor
func perform() async throws -> some IntentResult & OpensIntent {
    if #available(iOS 18.1, *) {
        guard let url = URL(string: <Universal Links>) else {
            return .result()
        }
        return .result(opensIntent: OpenURLIntent(url))
    } else {
        // OpenURLIntentを使わない遷移処理
    }
}

OpenURLIntentは、iOS 18.0から利用可能ですが、アプリに直接遷移できないなどの不具合がありました。これらは、iOS 18.1の修正で解決されています。そのため、安定した動作を求める場合はiOS 18.1以上で使用する必要があります。

OpenURLIntent

OpenURLIntentは条件が整えば非常に便利ですが、iOS 18.1未満でも使用する場合は、上記のコードのようにOSバージョンによる分岐を入れる必要があります。
マネーフォワード MEでは、App Shortcutsなどで特定のApp Intentを使用しています。そのため、分岐処理を入れる必要がある点や、Universal Linksに対応する必要がある点などを考慮し、今回はOpenURLIntentを使用しない方針としました。

他の実装方法としては、<特定の画面に遷移する処理>の部分で一時的に遷移先を特定する情報をローカルDBやシングルトンで保持し、SceneDelegateなどでハンドリングする方法や、NSNotificationCenterを使用する方法があります。
例えば、以下のような実装が考えられます。

struct ExampleIntent: AppIntent {
    static var title: LocalizedStringResource = "Example"
    static var openAppWhenRun: Bool = true
    
    @MainActor
    func perform() async throws -> some IntentResult {
        // Custom URL SchemeのURLを一時的にローカルDBやシングルトンで保持する
        AppIntentManager.shared.destinationURL = <Custom URL Scheme>
        return .result()
    }
}
final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    func sceneDidBecomeActive(_ scene: UIScene) {
        // App Intentから画面遷移に必要な情報がある場合のみハンドリングする
        if let url = AppIntentManager.shared.destinationURL {
            // 値をリセットする
            AppIntentManager.shared.destinationURL = nil

            <URLを元に画面遷移処理を行う>
        }
    }
}

他にも方法はあると思いますので、アプリの要件や環境に応じて最適な方法を選択しましょう。

1-3-2. Control Widget

struct ExampleWidgetControl: ControlWidget {
    static let kind: String = "com.example.ExampleControlWidget"

    var body: some ControlWidgetConfiguration {
        StaticControlConfiguration(
            kind: Self.kind
        ) {
            ControlWidgetButton(action: ExampleIntent()) {
                Image("example_icon")
            }
        }
        .displayName("ホーム画面を開く")
        .description("Exampleアプリを開きます")
    }
}

こちらもApp Intent同様に、ControlWidgetのインターフェースに準拠する形で実装します。
actionには、先ほど作成したExampleIntent()を設定します。
今回はアプリを開くだけのコントロールになるので、StaticControlConfigurationを使用し、クロージャ内でコントロールのUIを設定します。

StaticControlConfiguration

実装のポイントは以下の3点です。

・Imageに使用できるのはシンボルイメージのみ

コントロール内で使用できるImageは、SF Symbolsのシンボルまたはカスタムシンボルといったシンボルイメージのみです。
シンボルイメージ以外を設定しても、何も表示されませんのでご注意ください。

HumanInterfaceGuideline/Controls#Anatomy

・使用するImageは対象のアプリと機能が伝わるものにする

SF Symbolsでも実装は可能ですが、他のコントロールと並んだ時にアプリ側で提供しているコントロールと直感的に区別するのが難しい場合があります。
マネーフォワード MEで提供しているコントロールは、アプリ内の各機能で使用しているアイコンとアプリアイコンを組み合わせることで、他のコントロールと並んでも認識できるように工夫しています。

特に、ホームやカメラのようなアイコンは被りやすいので、実装する際は意識してみると良いかもしれません。

・displayNameはコントロール内で表示される

displayNameで設定しているタイトルは、コントロール内やコントロール一覧で表示されます。
displayNameを設定しない場合は、アプリ名になるようなので、正常に設定できているか確認しましょう。

2. フィーチャーのノミネート機能

今回のコントロールウィジェットのリリースに際し、フィーチャーのノミネート機能を活用しましたので紹介します。

App Storeでは、特定のアプリがTodayタブやAppタブなどで紹介されることがあります。
これを「フィーチャー」と呼びます。
今回、そのノミネートをApp Store Connect内で行えるようになりました。

App Storeでのフィーチャー

2-1. どこで掲載されたのか

マネーフォワード MEは、アプリタブの最上部のカルーセルバナーで紹介されました。
これにより、アプリのプロダクトページ以外でも露出が増え、多くの方にアプリを知っていただけるようになります。

2-2. ノミネート機能を使ってみた感想

ノミネート機能を活用する際、推したい機能やアップデート内容が決まっていれば、必須の入力欄は10分程度で完了します。
非常にシンプルなので、推したい内容がある方にはぜひ活用をおすすめします。

2-2-1. 意識したこと

必須の入力欄以外にも任意で設定できる項目がありますが、全て埋めることを意識しました。
文字数は限られているため、全てをフル活用して推したい機能の魅力を最大限に伝えるよう心がけました。

2-2-2. 継続的なアップデートの重要性

今回フィーチャーされたのは、新しい取り組みだけでなく、これまで続けてきたアップデート全体が評価された結果だと考えています。
アップデートには機能的な改善だけでなく、新しい取り組みをスムーズに行うためのメンテナンスも含まれています。

例えば、マネーフォワード MEでは定期的にOSサポートバージョンのメンテナンスを行い、最新の2バージョンのOSを基本的にサポートしています。
このような取り組みにより、OSの分岐を最小限に抑えられるなどメンテナンスが容易になり、新しい機能も導入しやすい環境になります。

しかし、サポートする最低OSバージョンを引き上げることは、新規インストールができなくなる端末が増えることを意味します。
そのため、エンジニアだけでなく、日頃から各関係者の理解と協力が不可欠だと改めて感じています。

2-2-3. 注意点

フィーチャーに関する通知には注意が必要です。
2024年12月現在、「Today」タブの特定のプレースメントに掲載されると、App Store Connectアプリ経由で通知が行われます。
しかし、「アプリ」タブで取り上げられた場合には、通知を受ける機能はまだ提供されていません。今回のケースでは、アプリのリリース当日にApp Storeアプリを見て、初めてフィーチャーされていることに気づきました。

そのため、通知がなかったとしても、リリース日(ノミネート申請フォームで指定した日)にはApp Storeの各タブを確認することをおすすめします。

App Storeでのフィーチャーに関するプロセスの改善

3. まとめ

マネーフォワード MEのよく使われる画面に簡単にアクセスできるコントロールウィジェットへの対応と、サービスの認知度向上につながるフィーチャーのノミネート機能について紹介しました。
今後も、ユーザーにとって便利で使いやすいアプリを提供するために、継続的な改善を続けていきます。

最後までお読みいただき、ありがとうございました。