booklista tech blog

booklista のエンジニアリングに関する情報を公開しています。

iOS16の新機能「ロック画面ウィジェット」を推し活アプリに導入してみた話

アイキャッチ

はじめまして。株式会社ブックリスタ プロダクト開発部の酒井です。
普段はスマホアプリエンジニアとして推し活アプリ「Oshibana」の開発を行なっています。
Oshibanaについては以前の記事で紹介をしていますので、ぜひともご一読ください。

App Storeで3位になったアプリをFlutterでつくっている話
https://techblog.booklista.co.jp/entry/2022/07/11/155531

iOS16について

2022年6月に開催されたWWDC2022にて、iOS16の新機能が発表されました。
今回iOS16ではロック画面のカスタマイズが強化されており、中でも「ロック画面にウィジェットを配置できる」という機能が注目されています。

Oshibanaでは推し活に役立つウィジェットを作成できる機能を提供しています。
弊社はiOS16の新機能であるロック画面ウィジェットに目を付け、Oshibanaで作成したウィジェットをホーム画面だけでなくロック画面にも配置できるようにすることで、より推しへのアプローチが増えると考え、いち早くOshibanaへ導入するため開発に着手していました。

この記事では、iOS16で追加された新機能である「ロック画面ウィジェット」の紹介と、 その機能をOshibanaへ組み込んだ方法について紹介していきます。

ロック画面ウィジェットの紹介

<概要>

iOSではホーム画面にウィジェットを配置できる機能がiOS14の頃より提供されています。
今回、iOS16からはこのウィジェットをロック画面にも配置できるようになりました。
ウィジェットはロック画面に固定で表示されている時計の上下に配置できます。
配置できるウィジェットの種類は以下の通りです。
・インラインウィジェット・・・時計の上に配置できるテキスト1行分のウィジェット
・円形ウィジェット・・・時計の下に配置できる円形のウィジェット
・長方形ウィジェット・・・時計の下に配置できる横長(円形2つ分)のウィジェット

ロック画面

上記画像はロック画面上にiOSの標準アプリである「カレンダー」「バッテリー」「アラーム」「天気」のウィジェットを配置してみたものです。
ホーム画面に配置できるウィジェットと比べ、それぞれサイズが非常にコンパクトなものとなっています。
これは今回のロック画面ウィジェットがApple Watchで配置できるウィジェットのデザインを踏襲しているものであり、腕時計のようにアプリのデータが一目で確認できることを目的としているためです。
例えばカレンダーなら直近の予定が一目でわかり、バッテリーなら電池の残量、アラームならアラームが鳴る時刻、天気なら気温が分かるようになっています。
このように、いかにユーザーへ素早く情報を伝えるかがロック画面に配置されるウィジェットの役割として重要なポイントとなります。

また、制約として、配置されるウィジェットは必ずモノクロのカラーリングとなります。
よって、画像は白黒となり、文字や線などに色をつけることができないので注意してください。

<ウィジェット配置方法>

以下の手順でロック画面にウィジェットを配置できます。
1. ロック画面を表示し、画面を長押し(※1)
2. 画面下に現れた「カスタマイズ」ボタンをタップ
3. 時計の上もしくは下のエリアをタップ
4. 表示されたアプリの一覧から配置したいウィジェットを持つアプリを選択
5. 配置したいウィジェットの種類を選択(※2)
6. アプリ一覧を閉じ、右上の完了ボタンをタップ
7. 「壁紙を両方に設定」ボタンをタップ

(※1) パスコードロックを解除しておかないと長押しを行っても反応しません。ホーム画面を表示した状態で端末の一番上から下に指でスライドさせるとパスコードロック解除済の状態でロック画面を表示させることができます。

(※2) 配置スペースが足りないと配置できません。ウィジェットを追加で配置する場合は既に配置済のウィジェットを削除しておく必要があります。

ロック画面 チュートリアル1ロック画面 チュートリアル2ロック画面 チュートリアル3ロック画面 チュートリアル4ロック画面 チュートリアル5



Oshibanaへの導入

Oshibanaでは様々なウィジェットが用意されていますが、今回は既存のウィジェットからロック画面のデザインに適応できそうな下記4つのウィジェットを選定し、開発しました。
・画像ウィジェット(インラインウィジェットでは表示不可)
・推し始めてウィジェット
・生誕からウィジェット
・デビューからウィジェット

ロック画面 Oshibanaイメージ

将来的には他のウィジェットの実装も検討しています。
しかし、ロック画面ウィジェットは前述の通りサイズが小さく、色がモノクロになるため、コンテンツを表示できる範囲に限界があり、画像の上にテキストを表示させるようなデザインだと見辛くなってしまうなどの問題があります。
なので、ウィジェットのデザインはシンプルなものが望ましく、表示される情報も即時性が高いものを優先させるなど工夫が必要であるため、慎重に検討を進めています。

※iOS16の機能を開発するためには、Xcode14以上が必要です。

Widgetターゲットの新規追加

最初にロック画面ウィジェット用のWidgetターゲットを追加します。
プロジェクトファイルのTARGETSで+ボタンを押下し、ターゲットの選択画面を表示し、「Widget Extension」を選択し、Nextを押下します。
2つ目の画面では、Product Nameを入力し、Include Configuration Intentのチェックを外し、ProjectとEmbed in ApplicationでRunnerを選択し、Finishを押下します。

※RunnerはFlutterでiOSアプリを作った際にデフォルトで作成されるプロジェクト名となります。

スクリーンショット1

スクリーンショット2

Widgetターゲットを追加すると、自動的にProduct Nameに設定した名称でフォルダとswiftファイルが作成されます。
自動追加されたswiftファイル内の各クラスやメソッドについてはホーム画面ウィジェットの作成時と同様のため、詳細な解説は省略し、ロック画面ウィジェットに関係ある部分のみ記載します。

サポートファミリーを定義

下記のように、supportedFamiliesに
・accessoryCircular
・accessoryRectangular
・accessoryInline
の3つを定義します。

struct oshibana_lock_widget: Widget {
    let kind: String = "oshibana_lock_widget"

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: Provider()) { entry in
            oshibana_lock_widgetEntryView(entry: entry)
        }
        .configurationDisplayName("ロック画面ウィジェット")
        .description("設置したいウィジェットを選択しましょう")
        .supportedFamilies([.accessoryCircular,.accessoryRectangular,.accessoryInline])
    }
}

この時点でロック画面ウィジェットは配置可能になっているので、シミュレーターか実機にデプロイし、動作確認を行なってみてください。

Intentsターゲットの新規追加

ロック画面に表示するウィジェットの種類をユーザーが選択できるようにするため、ロック画面ウィジェット用のIntentsターゲットを追加します。

スクリーンショット3

スクリーンショット4

プロジェクトファイルのTARGETSで+ボタンを押下し、ターゲットの選択画面を表示し、「Intents Extension」を選択し、Nextを押下します。
2つ目の画面では、Product Nameを入力し、Include UI Extensionのチェックを外し、ProjectとEmbed in ApplicationでRunnerを選択し、Finishを押下します。

スクリーンショット5

スクリーンショット6

Intentsターゲットを追加すると、Widgetターゲットの時と同様にフォルダとswiftファイル(デフォルト名はIntentHandler.swift)が作成されます。
こちらも作成されたクラスやメソッドの内容はホーム画面ウィジェットの作成時と同じであるため、ロック画面ウィジェットに関係する部分以外の解説は省略します。

Intentsターゲットを追加したら、IntentsターゲットページのSupported Intentsにアイテムを追加します。
既存でホーム画面ウィジェット用のIntentsが存在しているため、別名で登録します。

スクリーンショット7

SiriKit Intent Definition Fileの作成

次に、作成されたロック画面用widgetフォルダの配下にSiriKit Intent Definition Fileを作成します。
ファイル選択後、Save Asにファイル名を入力し、TargetsでRunnerとロック画面のwidgetターゲットとIntentターゲットを選択してCreateボタンを押下します。

スクリーンショット8

スクリーンショット9

作成された.intentdefinitionファイルを開き、下記画像のように設定します。
画像ではParametersで独自のパラメータを定義してますが、作成したいウィジェットの内容に合わせて変更してください。

スクリーンショット10

IntentHandlerを構成

.intentdefinitionファイルを設定した後は、ウィジェット表示内容の選択肢を定義するため、IntentHandler.swiftの修正を行います。

※下記はサンプルのため、一部省略しています。

class IntentHandler: INExtension, LockConfigurationIntentHandling {

    func provideLockwidgettypeOptionsCollection(
        for intent: LockConfigurationIntent,
        with completion: @escaping (INObjectCollection<LockWidgetType>?, Error?) -> Void) 
    {

        var widgetTypes: [LockWidgetType] = []
        widgetTypes.append(LockWidgetType( 省略 ))

        let allCatIdentifiers = INObjectCollection(items: widgetTypes)
        completion(allCatIdentifiers, nil)
    }

    override func handler(for intent: INIntent) -> Any {
        return self
    }
}

provideLockwidgettypeOptionsCollection関数内に.intentdefinitionファイルで定義したパラメータのリストをIntentに対して設定する処理を実装します。
ここでリストを定義することで、ウィジェットをタップした際に動的な選択肢を表示させることができます。
上記例では省略していますが、Oshibanaではアプリで入力したウィジェットの設定値をローカルのDBに保存し、provideLockwidgettypeOptionsCollection関数内で保存したデータを呼び出してリストに設定しています。

IntentTimelineProviderを継承

次に、Widgetターゲット作成時に自動生成されたswiftファイルの中身をIntentsに適用させるよう変更します。

※下記はサンプルのため、一部省略しています。

struct Provider: IntentTimelineProvider {
    typealias Intent = LockConfigurationIntent

    @available(iOSApplicationExtension 16.0, *)
    func recommendations() -> [IntentRecommendation<LockConfigurationIntent>] {
        []
    }

    func placeholder(in context: Context) -> OshibanaEntry {
        let conf = LockConfigurationIntent()
        return OshibanaEntry(date: Date(), configuration: conf)
    }

    func getSnapshot(for configuration: LockConfigurationIntent, in context: Context, completion: @escaping (OshibanaEntry) -> ()) {
        let entry = OshibanaEntry(date: Date(),configuration: configuration)
        completion(entry)
    }

    func getTimeline(for configuration: LockConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        let entry = OshibanaEntry(date: Date(),configuration: configuration)
        let timeline = Timeline(entries: [entry], policy: .atEnd)
        
        省略

        completion(timeline)
    }
}

struct oshibana_lock_widget: Widget {
    let kind: String = "oshibana_lock_widget"

    var body: some WidgetConfiguration {
        return IntentConfiguration(kind: kind, intent: LockConfigurationIntent.self, provider: Provider()) { entry in
            return oshibana_lock_widgetEntryView(entry: entry)
        }
        .configurationDisplayName("ロック画面ウィジェット")
        .description("設置したいウィジェットを選択しましょう")
        .supportedFamilies([.accessoryCircular,.accessoryRectangular,.accessoryInline])
    }
}

デフォルトではTimelineProviderが継承されていましたが、IntentTimelineProviderを継承するよう変更します。
IntentTimelineProviderへの変更に応じて、placeholder、getSnapshot、getTimelineの引数と戻り値の定義を変更し、typealiasを宣言します。
WidgetConfigurationでreturnしているオブジェクトもStaticConfigurationからIntentConfigurationに変更します。

ロック画面ウィジェットにおける基本的な実装は以上です。
後はEntryViewでウィジェットのデザインを整え、Timelineからパラメータを受け渡してウィジェット毎に表示結果を変更するなどの機能追加を行なっていけば良いと思います。

開発中に苦労したこと

1.Xcode14で実機ビルドするとエラーが発生した

Xcode14をインストールし、ソースをビルドすると以下のエラーが発生しました。

error build: '/Users/User/Library/Developer/Xcode/DerivedData/Runner-ftqsuopsbckjgpfaojzqmthsozpo/Build/Products/Debug-iphoneos/Alamofire/Alamofire.framework/Alamofire' does not contain bitcode. You must rebuild it with bitcode enabled (Xcode setting ENABLE_BITCODE), obtain an updated library from the vendor, or disable bitcode for this target. file '/Users/User/Library/Developer/Xcode/DerivedData/Runner-ftqsuopsbckjgpfaojzqmthsozpo/Build/Products/Debug-iphoneos/Alamofire/Alamofire.framework/Alamofire' for architecture arm64

Oshibanaで利用しているライブラリの1つであるAlamofireでエラーが発生していましたが、ここでは直接関係は無く、原因は「Xcode14からはbitcodeが非推奨になったため」でした。

[参考サイト]
Xcode 14 Release Notes

対策として、TARGETSからRunnerを選択し、Build SettingsのEnable BitcodeをYESからNOに変更すれば解消しました。

2.iOS15.5以下の端末でアプリを起動させたら、ホーム画面に設置済だったウィジェットが全て真っ白になった

iOS15.5以下の端末にロック画面ウィジェットの対応が入ったアプリをインストールしたところ、ホーム画面に設置済だったウィジェットが全て真っ白になる、及びウィジェット配置時のアプリ一覧にOshibanaが表示されなくなる事象が発生しました。

iOS16からIntentRecommendationというクラスが追加されており、watchOSにてIntentを設定する際、推奨インテント構成を記述するためのメソッドであるrecommendations関数を実装する必要があリます。
今回はiOSのみの対応であるため本来なら不要な処理ですが、おそらくXcode14では上記クラスをOSの種類やバージョンに関わらず呼ぶようなデプロイが行われており、iOS15では上記クラスが見つからずにエラーが発生し、WidgetKitフレームワークのシンボルが見当たらないとされ、Oshibanaのウィジェット機能そのものが起動しなくなってしまったものと思われます。
(今後のバージョンアップによって修正される可能性はあります)

対策として、以下のコードを既存のホーム画面ウィジェットの処理であるProvider内にも追加しました。

@available(iOSApplicationExtension 16.0, *)
func recommendations() -> [IntentRecommendation<Intent>] {
    return []
}

[参考サイト]
AppleDeveloperフォーラム #709233
Apple公式documentation - IntentRecommendation

3.VSCodeでビルドエラーになり、シミュレーターが起動できず、デバックができなくなった

Oshibanaのアプリ側はFlutterで実装しているため、VSCodeで開発していましたが、ビルド時にエラーが発生するようになりました。
原因はbeta版のXcodeを開発に使っており、VSCodeのビルドで指定するXcodeが正規版(バージョン:13.4)の方になっていたため、iOS16で新規追加されたAPIがビルドできずエラーになっていました。
下記コマンドをターミナルで実行し、Xcode13.4ではなくXcode14betaを指定するよう変更すればビルドできるようになりました。

sudo xcode-select -s /Applications/Xcode-Beta.app

初歩的なミスでしたが、Xcodeのbeta版を使っていたり、複数バージョンを別名にして使い分けていたりすると、よく忘れる内容だと思います。

4.Codemagicで本番用アプリのデプロイを行うとエラーになった

Oshibanaではipaを生成する際、Flutterで作ったアプリのデプロイを自動化できるCIツール「Codemagic」を採用しています。
開発用ipaのデプロイを行なった時は問題なくipaが作成されたのですが、AppleStoreに申請する本番用ipaを作成するためビルドを行なった際、以下のエラーが発生しました。

Error (Xcode): No signing certificate "iOS Development" found: No "iOS Development" signing certificate matching team ID "XXXXXXXX" with a private key was found.

Xcode14からビルドオプションの「CODE_SIGNING_ALLOWED」のデフォルト値が"NO"から"YES"になったらしく、ビルド時にコード署名を行おうとしますが、Codemagicで署名しようとすると署名証明書が見つからずエラーになってしまうようです。

対策として、以下のコードをPodfileに追記しました。

post_install do |installer|
  installer.generated_projects.each do |project|
      project.targets.each do |target|
          target.build_configurations.each do |config|
              config.build_settings["DEVELOPMENT_TEAM"] = "XXXXXXXX"
          end
      end
  end
  installer.pods_project.targets.each do |target|
      if target.respond_to?(:product_type) and target.product_type == "com.apple.product-type.bundle"
        target.build_configurations.each do |config|
            config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO'
        end
      end
  end
・・・

「CODE_SIGNING_ALLOWED」を“NO”にする設定を追加することにより、ビルド時にコード署名が行われなくなり、ビルドが通るようになります。
コード署名されてないipaはリリースできませんが、Codemagicの「Build」フェーズの後続に「Distribution」フェーズがあり、そこで自動コード署名を行なっているため、問題ありません。

[参考サイト]
Github CocoaPods issue #11402

5.ロック画面ウィジェットの対応が入ったアプリをAppleStoreに申請してみたが、申請できなかった

iOS16のbeta版が配布されているため、お試しで使える方に使ってもらおうとロック画面ウィジェット機能を導入したバージョンのOshibanaをAppleStoreに申請してみましたが、申請自体ができませんでした。

AppleStore

そもそもbeta版での開発内容(Apple Beta Software Program)は機密情報に該当するため、言われてみればそりゃそうだという感じでした。
この件について認知していない方は意外と少なくはないかもしれないと思ったので、記載させて頂きました。
参考:Apple Beta Software Program よくある質問

やってみた感想

今回の開発はbeta版が提供された時点から着手をしていました。
やはり挙動も少し不安定で、上記で記載した「苦労したこと」以外にも、Oshibana特有の仕様に影響する不具合もいくつか発生し、ブログには記載しづらい部分でも多くの苦労がありました。
何より、新機能ということでネット上に情報が少なく、解消方法を見つけるのも大変で、数時間前にAppleDeveloperフォーラムへ書き込まれた内容を参考に問題が解決したということも多々ありました。

しかし、新しい機能を実際に試して動かせるというところにとても新鮮味があり、なかなか面白い経験をすることができました。
更に、どこよりも早く新機能を実践投入できることへの期待感も高く、苦労したこと以上に楽しさを見いだすことができ、多くの知見を得られました。

2022年夏時点では、iOS16の開発に関する情報も日本だとまだ多くないため、この記事が皆さんのiOSアプリ開発に役立つことを願っています。