every Tech Blog

株式会社エブリーのTech Blogです。

ヘルシカにウィジェットを新規追加した話

はじめに

こんにちは!株式会社エブリーで1ヶ月間 iOS アプリエンジニアとしてインターンをしている白井です。この記事では、「ヘルシカ」というヘルスケアアプリ開発において取り組んだ、「ウィジェット機能」の実装についてお話ししたいと思います。

ヘルシカとは?

本題に入る前に、今回開発に取り組んだ「ヘルシカ」とはどんなアプリでしょうか?ヘルシカとは、ダイエットや健康のために、ユーザーの食事や体重の記録を手助けするアプリです。

ヘルシカ - ダイエット&健康のためのカロリー管理

ヘルシカ - ダイエット&健康のためのカロリー管理

  • every, Inc.
  • ヘルスケア/フィットネス
  • 無料
‎「ヘルシカ - ダイエット&健康のためのカロリー管理」をApp Storeで

具体的な機能として、体重の記録や、朝食・昼食・夕食・間食の食事内容を記録することができ、下の画像のように、その日の食事の摂取カロリーや、糖質、脂質などの栄養素の摂取状態について知ることができます(各栄養素の情報はプレミアムユーザーのみ閲覧可能)。

また、これに加えて、プレミアムユーザーは1食分の食事記録、無料ユーザーは3食分の食事記録をすることで、管理栄養士からの食事のアドバイスをもらうことができ、日常的に自分の食生活について見直す手助けもしてくれます。

背景

そんなヘルシカですが、現在開発チームは、「ユーザーの継続率向上」を目指し、新機能の追加や既存機能の改善に取り組んでいます。今回、その施策の一つとして、日々の食事の記録をより日常に溶け込ませるためのウィジェット機能を開発しました。

ウィジェットとは?

ウィジェットとは、スマートフォンのホーム画面に追加できるコンポーネントのことです。これにより、ユーザーはアプリを起動しなくても、今日の予定などのデータをホーム画面から直接確認できるようになったり、ウィジェットをタップするだけでアプリ内の特定の画面に素早く移動できるようになります。

今回開発したウィジェットの2つの要件

今回ヘルシカで開発したウィジェットは、食事記録を習慣化しやすくするために、以下の2つの要件を組み込みました。

  • 食事記録画面への直接遷移:ホーム画面からワンタップで食事記録画面に遷移し、すぐに記録を開始できる
  • 総摂取カロリーの常時表示:一日の総摂取カロリーをホーム画面に常に表示し、ホーム画面を見るだけで現在のカロリー状況をすぐに把握できる

これらの機能により、ヘルシカのメイン機能である「食事記録」がユーザーにとってより手軽になり、振り返りも簡単になることが期待され、この体験を通じて、ユーザーが食事記録をより日常の習慣として継続しやすくなることを目指しています。

以下が、ウィジェットの実際のデザインです!

では、本題であるそれぞれの機能の実装について見ていきましょう。

注: ウィジェットの基本的な実装方法についてはこの記事ではあまり触れません。代わりにこちらの記事をご参照ください。

iOS版デリッシュキッチンにウィジェット機能を追加しました - every Tech Blog

食事記録画面への直接遷移

まず、「食事記録画面への直接遷移」についてです。この機能を実装するには、DeepLink という仕組みを利用します。詳細には触れませんが、DeepLinkは、ウェブサイトのURLのように、アプリ内の特定の画面へ直接ユーザーを誘導する機能です。

実装のプロセス

ウィジェットで DeepLink の機能を利用するためには、以下の2つのステップが必要です。

  • ウィジェット側での Link の設定: ウィジェット内で、各食事タイプ(朝食・昼食・夕食・間食)に対応する URL を設定し、それらの URL を用いて Link という UI コンポーネントを用意します。これにより、ユーザーが Link をタップした場合、そのタップが特定の URL を呼び出すトリガーとなります。
  • アプリ側での DeepLink のハンドリング: ウィジェット から送られてきた DeepLink を検知するために、アプリの SceneDelegate での処理の設定を行います。ヘルシカでは、NotificationCenter を使って、食事記録画面の管理の責務を持つ ViewController に通知を送ることで、目的の画面を開くようにしています。

つまり、以下の図のような流れでウィジェットからアプリへと遷移し、食事記録画面を開くことを実現しています。

コード例は以下です(実際のコードとは異なり簡略化しています)。

  • ウィジェット(Link)
  HStack {
    Link(destination: URL(string: "https://myapp.com/record?type=morning")!) {
      // UIを定義するコード
    }
    Link(destination: URL(string: "https://myapp.com/record?type=lunch")!) {
      // UIを定義するコード
    }
    // 夕食の Link ...
  }
  • アプリ (SceneDelegate, NotificaitonCenter)
  class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    // ...
    func scene(_: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
        for urlContext in URLContexts {
            // ここの DeepLink はカスタムの enum であり、 url を扱いやすくしている。
            guard let deepLink = DeepLink(url: urlContext.url) else { continue }

            switch deepLink {
            // その他の DeepLink のハンドリング
            // ...
            case .mealRecord(let type):
                NotificationCenter.default.post(
                    name: .widgetMealRecordEditRequest,
                    object: nil,
                    userInfo: ["mealType": type]
                )
            }
        }
    }
    // ...
  }

このように「食事記録画面への直接遷移」の実装は、比較的シンプルに終えることができました。

総摂取カロリーの常時表示

次に、今回の実装で面白い部分である「1日の総摂取カロリーの常時表示」について解説します。

別プロセスで動くアプリとウィジェット

一見、「摂取カロリーを表示する」という機能は非常にシンプルに見え、アプリ側で取得している摂取カロリーのデータをそのままウィジェットで利用すれば良いだけなのでは?と思われるかもしれません。しかし、以下の理由からウィジェットはアプリのデータを直接利用することはできません。

  • アプリとウィジェットはそれぞれ独立な環境を持つ

共有できないインメモリなデータ

アプリとウィジェットがそれぞれ独立な環境を持つことは、今回の実装において何が問題なのでしょうか?独立な環境を持つということは、ライフサイクル管理やプロセスの管理がアプリとウィジェットで独立しているということです。そして、プロセスはそれぞれで独立したメモリ空間を持つため、プロセス間で直接データを共有することはできません。そのため、別プロセス上で動くアプリの取得した摂取カロリーのデータを、ウィジェットは用いることはできません。

また、インメモリなデータである摂取カロリーのデータは、アプリを閉じてしまうと消えてしまう揮発性のデータでもあるため、そもそも同じプロセス上で動いていたとしても、アプリを閉じると摂取カロリーのデータが失われ、そのデータをウィジェットに表示できなくなってしまいます。

認証を必要とするデータ取得プロセス

では、どうすればウィジェットで摂取カロリーのデータを取得できるでしょうか?今度は、ウィジェットが直接APIを呼び出してデータを取得すれば良いのでは? と思うかもしれません。しかし、これにも大きな課題があります。

表示したいデータに着目してみましょう。今回表示したい摂取カロリーのデータは、ユーザーが個別で記録している個人情報です。そのため、プライバシーの観点から、API でデータを取得するには、ユーザーがそのデータにアクセスして良いかを確認する認証のプロセスが必要となります。しかし、認証に必要なデータは、現在アプリによって管理されているため、ウィジェットは認証に必要なデータにアクセスができず、摂取カロリーのデータを取得することができません(下図を参照)。では、ウィジェットはどのように摂取カロリーのデータを取得すれば良いのでしょうか?

解決法①:Keychain によるデータの直接共有

単純な解決方法としては、Apple から提供されている Keychain というデータベースを通じて、アプリが取得した摂取カロリーのデータをそのままウィジェットに共有する方法があります(下図を参照)。Keychain では、データはすべて暗号化されてから保存されるため、個人情報の保存も問題ありません。しかし、この方法には問題点があります。この方法では、ウィジェットはデータの読み取りのみを行い、API の呼び出しは全てアプリ側で行われます。そのため、データの更新がアプリに依存し、アプリでの更新が起きなければウィジェットのデータは常に古いままになってしまいます。更新があまり必要のないデータであればこれはあまり問題にはならないかもしれませんが、ヘルシカの場合、カロリーのデータは日付が変わったタイミングで更新される必要があります。そのため、更新のタイミングがアプリに依存し、古いデータが表示される可能性もあるウィジェットは、ヘルシカのユーザー体験として望ましいものではありません。

注:図では、アプリと Keychain との認証データの受け渡しのフローを省略していますが、この場合でもアプリと Keychain 間で認証データの受け渡しは行われています。

解決法②:Keychain による認証関連データの共有

そこで、今回採用した解決手法は、Keychain を通じて認証に必要なデータを共有し、ウィジェットから認証付きの API を直接呼び出す手法です(下図を参照)。この手法を用いることで、先ほどのデータの更新タイミングがアプリに依存するという問題点を解決しつつ、ウィジェットで常に最新データを取得することも可能になります。

解決法②の実装

では、Keychain を利用したアプリ・ウィジェット間のデータ共有の実装方法についてみていきましょう。Keychain を通じてデータを共有するためには、Keychain Sharing という機能を使用します。

Keychain Sharing とは?

Keychain Sharing は、その名の通り、複数のアプリ間において Keychain のアイテムを共有する方法のことです。前提として、デフォルトでは、各アプリごとに一つの Keychain を管理する領域 (Keychain Group) が与えられており、他のアプリは原則その領域のデータを見ることはできません。しかし、Keychain Sharing を用いて複数アプリ間で共有可能な Keychain Group を作成することにより、複数のアプリで安全にデータの共有ができるようになります。

Keychain Sharing の設定

Keychain Sharing を使用するには、アプリ・ウィジェット両方において、Keychain Sharing の設定を行う必要があります。以下のステップで設定を行いましょう。

  1. ターゲットのリストからアプリ(ウィジェット)のターゲットを選択し、”Signing & Capabilities” タブをクリックします。
  2. 左上の “+ Capability” (2025/09/11 現在) をタップし、”Keychain Sharing” を選択、追加します。
  3. ”Signing & Capabilities” タブ内に、Keychain Sharing のセクションが出現し、Keychain Groups が設定できるようになります。
  4. Keychain Groups の + ボタンをタップし、共有したい Keychain Group 名を入力します。(既存の Group でも、新規で作成する Group でもどちらでも共有可能です。)

この設定をウィジェット、アプリの両方で行い、同一の Keychain Group を設定することで、アプリ・ウィジェット間での Keychain のデータの共有が可能になります。

Keychain Group からの取得

Keychain Sharing の設定が完了したら、実際に Keychain からデータを取得する処理を実装します。OSSのライブラリも存在しますが、ここではネイティブでの実装方法として、データの「追加」を一例に解説します。

パスワードを保存するケースを考えましょう。Keychain へのパスワード保存は、Apple が提供する SecItemAdd メソッドによって実現できます。具体的には、以下のコードのように、まず保存するデータの属性を定義するクエリを作成し、このクエリを使って先ほどのメソッドを呼び出すことによって、Keychain にアイテムを追加できます。また、Keychain Sharing で指定した Keychain Group をクエリに設定することで、アプリとウィジェットで共有されている Keychain Group への書き込みが可能になります。

import Foundation
import Security

func savePasswordToKeychain(service: String, account: String, data: Data, accessGroup: String) {
    // 保存するデータの属性を定義するクエリ
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword, // 保存するデータの種類を指定(今回はパスワード)
        kSecAttrAccount as String: account,             // ユーザーを区別するためのアカウント名
        kSecAttrAccessGroup as String: accessGroup,     // 保存先のアクセスグループ
        kSecValueData as String: data                   // 実際に保存するデータ
    ]
    
    // クエリを使用してKeychainにアイテムを追加
    let status = SecItemAdd(query as CFDictionary, nil)

    switch status {
        // エラーハンドリングなどを記述
    }
}

これと同様に、「アイテムの削除」、「アイテムの更新」、「アイテムの検索」についても実装することで、Keychain 内のデータの管理ができるようになります。

これらの設定や実装をすべて終えることで、認証関連のデータをアプリとウィジェットで共有でき、ウィジェットからでも摂取カロリーデータを取得できるようになったため、ようやく「1日の総摂取カロリーの常時表示」という要件を満たすことができました!

まとめ

この記事では、1ヶ月間のインターンで行った、ヘルスケアアプリ「ヘルシカ」における「ユーザーの継続率向上」に向けた施策である「ウィジェット機能の追加」についてまとめました。

今回は、「食事記録画面への直接遷移」と「総摂取カロリーの常時表示」という2つの要件を満たすために、以下の機能を用いました:

  • DeepLink:ウィジェットからアプリ内の特定画面への直接遷移を実現
  • Keychain Sharing:アプリとウィジェット間での安全なデータ共有を実現

特に「総摂取カロリーの常時表示」では、アプリとウィジェットが別プロセスで動作するという制約や、認証が必要な個人データの取得という課題に直面しましたが、これらの課題を Keychain Sharing を活用した認証データの共有によって解決し、ウィジェットから直接 API を呼び出すことで、常に最新のデータを表示できるようになりました。

エブリーでの1ヶ月間のインターンでは、ウィジェット機能をゼロから設計・実装し、期間内に無事リリースまで完了することができました。新機能の実装、そして実際のユーザーに届けるまでの一連の流れを経験できるインターンシップは貴重な機会だと思います。新しい技術にチャレンジしながら、実際のプロダクトに影響を与える開発に携わりたい方は、ぜひエブリーのインターンに応募してみてください!

参考資料