こんにちは、マルチデバイスチームでモバイルアプリエンジニアをしている小林 (@bakobox) です。
先日、m3.com iOSアプリにホーム画面とロック画面用のウィジェットを実装しました。 エムスリーのiOSアプリでは初の導入ということもあり、ウィジェット実装に関する知見が得られたので共有いたします!
ウィジェット導入の背景
m3.comアプリは医療従事者向けのプラットフォームで、最新の医療情報を収集できるアプリです。
アプリ内では、ニュースや医薬品、講演会など医療に関する様々な情報がチェックできたり、他の医療従事者と情報交換を行える場が提供されています。 忙しい医療現場でも、m3.comアプリを活用することで効率的に情報収集を行い、より良い医療提供につなげることができます。

3年ほど前にアプリのリファクタリングを行った後、新規機能や施策の開発が活発になり、またデザインリニューアルも行ってきました。
その流れで、より気軽にm3.comアプリへアクセスしていただけるようにウィジェットを開発することになりました。
導入したウィジェット
今回追加したのは次のウィジェットです。
ホーム画面用




ナビゲーションウィジェットは、アプリ内の特定のサービスへ素早くアクセスできるウィジェットです。 バッジにより、新着メッセージや放送中の講演会があるかどうかもひと目で分かります。
イチ推しウィジェットは、アプリ内のおすすめコンテンツを表示するウィジェットです。 各ユーザーにパーソナライズされています。
ロック画面用


こちらもホーム画面と同様に、特定のサービスへ素早くアクセスしたり、おすすめコンテンツを表示するウィジェットを追加しました。
実装時のTips
ウィジェットの導入方法に関しては様々な情報が既にあるため、ここでは少し工夫が必要なポイントをいくつかピックアップして紹介いたします。
Widget Extensionを追加する方法(XcodeGenを利用している場合)
m3.comアプリではXcodeGenを利用してプロジェクトを管理しているため、この場合の設定方法をご紹介します。
はじめにXcodeでファイルを生成する
Xcode -> File -> New -> Target
でWidgetExtensionを作成し、必要なファイルを生成します。
SampleWidget ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ └── WidgetBackground.colorset │ └── Contents.json ├── Info.plist ├── SampleWidget.swift └── SampleWidgetBundle.swift
project.ymlを編集
targetの追加
name: SampleApp ... targets: # アプリ本体 SampleApp: type: application ... dependencies: - ... - target: SampleWidget # 追記 # ウィジェット SampleWidget: type: app-extension platform: iOS dependencies: - sdk: SwiftUI.framework - sdk: WidgetKit.framework sources: - name: SampleWidget path: SampleWidget scheme: {} settings: base: PRODUCT_BUNDLE_IDENTIFIER: com.example.SampleWidget INFOPLIST_FILE: SampleWidget/Info.plist SKIP_INSTALL: true PRODUCT_NAME: $(TARGET_NAME) ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME: AccentColor ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME: WidgetBackground
schemeを追加
schemes: SampleWidget: analyze: config: Debug archive: config: Release build: targets: SampleApp: all SampleWidget: all profile: config: Release askForAppToLaunch: true run: config: Debug askForAppToLaunch: true debugEnabled: false environmentVariables: - variable: _XCWidgetKind value: isEnabled: false - variable: _XCWidgetDefaultView value: timeline isEnabled: false - variable: _XCWidgetFamily value: systemMedium isEnabled: false test: config: Debug
targetsのscheme: {}は一見必要ないように思えますが、これを省くとSampleWidget.xcschemeでwasCreatedForAppExtensionがYESに設定されず、Xcode上でApp Extensionであることが正しく認識されなくなります。*1


これをベースにして、後はプロジェクトに合わせて適宜修正していただけたらと思います。
本体側とのデータ共有について
ウィジェットは App Extension なので、アプリ本体とはプロセスが別です。 そのため、データ共有は少し工夫が必要です。
UserDefaults
アプリ本体の UserDefaults.standard とウィジェット側のUserDefaults.standardはデータが共有されていません。
共有したい場合は、App Groupsを使う必要があります。
init(suiteName:) | Apple Developer Documentation
これを用いてUserDefaultsを取得することで、データ共有が可能となります。
UserDefaults(suiteName: "{App Groupsのidentifier}")
HTTPCookieStorage
HTTPCookieStorage.sharedも同様に共有されないので、App Groupsを使う必要があります。
sharedCookieStorage(forGroupContainerIdentifier:) | Apple Developer Documentation
こちらを利用してHTTPCookieStorageを取得します。
HTTPCookieStorage.sharedCookieStorage(forGroupContainerIdentifier: "{App Groupsのidentifier}")
ユーザーがウィジェットを設置しているかどうかを調べたい
アプリ本体からウィジェットの利用状況を調べたい場合、WidgetCenterのgetCurrentConfigurations(_:)で取得可能です。
getCurrentConfigurations(_:) | Apple Developer Documentation
WidgetInfoの配列が返ってきます。これが、現在ユーザーが設置しているウィジェットです。
ユーザーの行動ログを荒らさないように気をつける
ウィジェットはアプリ本体が起動していなくてもデータ取得が行われるため、API呼び出しに基づいてユーザーの行動ログを取っている場合は注意が必要です。
例えば、最終アクセス時刻をAPIが最後に呼び出された日時で計測している場合、ウィジェットからのアクセスも混ざってしまい、最終アクセス時刻が意図していないものになる可能性があります。
ウィジェットからのアクセスを区別して記録するなどの対策が必要です。
特定時刻にデータを更新する実装を行っている場合には、スパイクに気をつける
ウィジェットで特定時刻にAPIを叩いてデータを更新するような場合、ウィジェットを設置している端末達がその時間に一斉にAPIを叩くことになります。
急激に負荷がかかってしまう可能性があるため、時間を少しずらして負荷を分散させるなど工夫する必要があるかもしれません。
ウィジェットだけでもダークモードに対応しておくのがおすすめ
アプリ本体でダークモード対応ができていない場合でも、ウィジェットはダークモードに対応しておきましょう。
ウィジェットを設置するのはホーム画面です。ホーム画面にはシステムUIや他のアプリのウィジェットも表示されているため、ダークモード対応をしていないとウィジェットが浮いてしまいます。
まとめ
ウィジェット導入に関するお話をさせていただきました。
リリースしてから1ヶ月ちょっと経ちましたが、利用してくださっているユーザーも徐々に増えてきていて嬉しいです!
We’re hiring !
エムスリーでは複数のiOSネイティブアプリを開発しており、Swift・iOSが好きなエンジニアを募集しております! ご応募お待ちしています!