Open21

AppleWatch: iPhone→AppleWatchにメッセージを投げる

kabeyakabeya

iPhoneからAppleWatchにメッセージを投げたいときがあります。

iPhoneでデータをいじったので、AppleWatch側で再取得してね、というようなケースです。
というか、AppleWatchのコンプリケーションをリロードさせたい。

Watch Connectivityでできるのか試しましたが、どうもWatchAppがフロントに居るときにしか、接続できないようです。(違ったらごめんなさい)
たぶん逆方向AppleWatch→iPhoneも、iPhoneAppがフロントに居ないとダメなんでしょうね。

あとはリモートNotificationかな、と思っていますが、メッセージとか出さずにできるのでしょうか。

kabeyakabeya

たぶん逆方向AppleWatch→iPhoneも、iPhoneAppがフロントに居ないとダメなんでしょうね。

AppleWatch→iPhoneはiPhone側アプリを起動すると書いてありました。

https://developer.apple.com/documentation/watchconnectivity/wcsession/1615687-sendmessage

Calling this method from your WatchKit extension while it is active and running wakes up the corresponding iOS app in the background and makes it reachable. Calling this method from your iOS app does not wake up the corresponding WatchKit extension.

とは言え、以下を読むと、WatchAppがバックグラウンドでもうまく動くように読めてしまうんですけども。

If you specify a reply handler block, your handler block is executed asynchronously on a background thread. The block is executed serially with respect to other incoming delegate messages.


追記

Watch→iPhoneは、確かにiPhoneアプリを終了していてもメッセージが届きます。iPhone側で受け取った時刻のタイムスタンプをつけてみると、割とWatchからの送信直後の時刻が打刻されます。
ただし、iPhone側が終了している場合は特に1回では成功しない可能性が高くて、数回リトライする必要がありそうです。(iPhone側が起動していても失敗するケースがあるので、リトライは必須かも)

リトライは以下の記事を参考にしました。

https://stackoverflow.com/questions/53984355/sometimes-watchconnectivity-session-on-paired-watch-simulator-is-not-reachable

kabeyakabeya

WCSession.transferUserInfo(_:)も試してみましたが、ダメでした。→訂正。OKそうでした。詳細は次のコメントで。

どうせ後で「どうだったっけ?」ってなるので「ダメだった」の内容を細かく書いておきます。

iPhone→WatchApp(≠WatchKit app)の転送に関して(Watch Connectivity=WCSessionを利用)

  • WCSession.sendMessage(_:replyHandler:errorHandler:)を利用した場合

    • WatchAppがアクティブでない場合
      iPhone側のsendMessageのerrorHandlerが割とすぐにconterpart app is not reachableで呼び出される。
    • WatchAppがアクティブな場合
      割とすぐに届いて、WatchApp側のWCSessionDelegate.session(_:didReceiveMessage:replyHandler:)が呼ばれる。
  • WCSession.transferUserInfo(_:)を利用した場合

    • WatchAppがアクティブでない場合
      何も起きない。詳しく言うと、おそらくバックグラウンドでWatch側にメッセージが転送されている。
    • WatchAppがアクティブな場合
      割とすぐに届いて、WatchApp側のWCSessionDelegate.session(_:didReceiveUserInfo:)が呼ばれる。
      WatchAppがアクティブでない間に転送された分も、WatchAppがアクティブになった瞬間に全部受け取られて、その数だけWCSessionDelegate.session(_:didReceiveUserInfo:)が呼ばれる。

なので、WatchAppがバックグラウンドかどうかによらず情報を送りつけて、Wacthコンプリケーションのデータを更新させるというのはWatch Connectivityでは厳しい気がします。

kabeyakabeya

WCSession.transferUserInfo(_:)でiPhone→WatchAppに転送した場合で、WatchAppがバックグラウンドにいる場合、WatchAppのWCSessionDelegate.session(_:didReceiveUserInfo:)が呼ばれるのに、3〜5分ぐらいかかります。
逆に言えば、それだけ待てばバックグラウンドでも届きます。
今回はいったんこれで良いことにしよう。

WCSession.sendMessage(_:replyHandler:errorHandler:)はすぐにエラーハンドラが呼ばれるのでおそらくダメだろうと推測しています。


追記
transferUserInfoは来るときは数十秒〜1分ぐらいで来ますが、来ないときは10分しても来ないですね。

kabeyakabeya

今回はいったんこれで良いことにしよう。

いや良くないな。
なんでしょう、標準のリマインダー.appとか、iPhoneで編集した結果がすぐにWatchコンプリケーションに反映される。
これはどうやってるのでしょうか。

kabeyakabeya

なんでしょう、標準のリマインダー.appとか、iPhoneで編集した結果がすぐにWatchコンプリケーションに反映される。
これはどうやってるのでしょうか。

リマインダーのWatchコンプリケーションを観察してみると、ずっと腕を下ろしていると更新されずに、腕を上げたら更新されるというような動きをしてます。

何かのハンドラでチェックしているような感じでしょうか。

kabeyakabeya

APNsを利用したリモート通知を色々調べてましたが、PushKitというのがその目的に合いそうです。

The PushKit framework supports specialized notifications for updating your watchOS complications, responding to file provider changes, and receiving incoming Voice-over-IP (VoIP) calls.

https://developer.apple.com/documentation/pushkit

普通のリモート通知とは違う、ということなんです。

PushKit notifications differ from the ones you handle with the User Notifications framework.

何が違うのかはこれから調べます。

kabeyakabeya

なんかPushKit、ダメですね。

self.registry = PKPushRegistry(queue: .main)
self.registry.delegate = self
self.registry.desiredPushTypes = [.complication]

こんな感じのコードを書けば、pushRegistry(_:didUpdate:for:)でデバイストークンを含むPKPushCredentialsを受け取れる、ということなんですが、pushRegistry(_:didUpdate:for:)が呼ばれません。

iOSで同じことをしようとしても.complicationはwatchOSだけよ?と言われるし、.fileProviderにするとpushRegistry(_:didInvalidatePushTokenFor:)が呼ばれてしまうし(これはよく調べてませんが.fileProviderを使う設定とかができてないのかなと思ってます)、なかなか進めません。

PushKitは次々機能縮小されている様子がうかがえることもあり[1]、PushKitはやめてサイレントリモート通知を使うべきという気がします。実際、VoIP受信時にCallKitを使いたくない場合はリモート通知を使え[2]、と書かれています。

脚注
  1. 例えばPKPushType.complicationはiOS/iPadOS 13.0でdeprecatedになっていますし、PKPushType.voIPを指定するとiOS/iPadOS 13.0以降でCallKitがリンクされていない場合は異常終了する(!)ように変更されています ↩︎

  2. If you don't want to post the CallKit call interface, handle notifications with the User Notifications framework instead of PushKit. (https://developer.apple.com/documentation/pushkit/pkpushtype/1614481-voip) ↩︎

kabeyakabeya

PushKitの.complication通知は、1日50通まででそれを超えると通知が来なくなる、と書いてあります(ヘッダにも)。

1日がどこから始まるのかよく分からない[1]し、来なくなるなら来なくなるで「もう届きませんよ?」というタイミングを教えてもらえないと困るのにその機能はないので、現実的には、来ても来なくてもどうでもよい通知にしか使えないような気がしています。

脚注
  1. APNsサーバに届いたタイミング?デバイスに通知したタイミング?UTC?デバイスのタイムゾーン?そもそも50通を誰がカウントしているのか?watchOS?APNs? ↩︎

kabeyakabeya

それでリモートプッシュ通知です。
Firebase Cloud Messaging(FCM)というのを使おうとしてハマりました。

具体的には、AppDelegateクラスを作って、その先頭でFirebaseApp.configure()を呼ぶのですが、こいつが、デバイストークンが通知されるメソッドである、自分のクラスのapplication(_:didRegisterForRemoteNotificationsWithDeviceToken:)を上書きしてしまうため、いつまでたってもデバイストークンを取れない、というので時間を取られました。

なんでapplication(_:didRegisterForRemoteNotificationsWithDeviceToken:)が呼ばれないのか、かなり悩みました。

もともと「手動で何かしたければ、Info.plistにFirebaseAppDelegateProxyEnabledを作って、NOを設定しろ」とコンソールにメッセージが出ていたのですが、手動で何かするのは後でいいんじゃないかと思っていたらこれです。

それがそういうことかと分かればなんてことはないのですけども。

ちなみに、これでNOを設定すると、application(_:didRegisterForRemoteNotificationsWithDeviceToken:)は上書きされず、デバイストークンが取れるようになります。

その代わりにFCMサーバにデバイストークンを知らせる処理を自分で書く必要があります(といってもMessaging.messaging().apnsToken = deviceTokenって1行書くだけですが)。

kabeyakabeya

状況を考えて、全体として以下のような構成にしようかと思いました。
これでうまく行くかはこれから検証します。

  • 前提

    • ソフトウェア構成
      • iPhone app:これが本体。メイン。
      • Realmデータベース:iPhone内に保存。これ自体を読み書きするのはiPhone appだけ。
      • CloudKitのプライベートデータベース:iPhone appとその他モジュールでやりとりするサブセットデータを管理。
      • iPhone widget:サブセットデータを表示。
      • Firebase Cloud Messaging(FCM):リモートプッシュ通知をするサーバ。
      • watchOS app:iPhone appのサブセットみたいなもの。サブセットデータをちょっとだけ操作可能。
      • watchOS app complication:サブセットデータ(のさらに一部をちょこっと)表示。
    • 各コンポーネントで連携すべき情報
      • CloudKit内のプライベートデータベースの中から、どのiPhoneの情報を操作すべきか(=どのiPhoneとペアになっているのか)を示す、iPhone識別子。
      • CloudKit内に保存してある、共有すべきサブセットデータ。
      • iPhoneまたはAppleWatchのどちらかで情報が変更されたら、そのタイミングを通知。
    • 考慮するポイント、考慮しなくてよいポイント
      • iPhoneとAppleWatchの両方で情報を変更できるけども、原則、原本はiPhoneのデータで、AppleWatchから原本を書き換えることはできないので、データに関する排他処理的なことは不要。watchOS app→iPhone appに通知を行い、通知に基づいてiPhone appが原本を更新する。iPhone app内でマルチスレッドで動くような部分にだけ気をつける。
  • 実際の処理

    • iPhoneで操作しているとき
      • iPhone app→iPhone widgetは、widget自身でiPhone識別子を調べられるので、単にWidgetCenter.shared.reloadAllTimelines()を実行。
        iPhone widgetはgetTimeline(in:completion:)内で調べたiPhone識別子に基づいてCloudKitからサブセットデータを取得して表示。
      • iPhone app→watchOS appはFCM経由でサイレントリモートプッシュ通知。ペイロードにiPhone識別子を付加。
        watchOS appは通知を受けたら、ペイロードから取得したiPhone識別子をApp GroupsのUserDefaultsに保存して、WidgetCenter.shared.reloadAllTimelines()を実行。通知ハンドラでは30秒までという話なのでこの辺までを実施。自分自身もそのiPhone識別子を使ってCloudKitから取得して表示更新する必要があるが、この処理は非同期で実行。
      • watchOS app→watchOS app complicationはgetTimeline(in:completion:)内でiPhone識別子をApp GroupsのUserDefaultsから取得し、それを使ってCloudKitからサブセットデータ取得して表示を更新。こっちは時間がかかっている間はプレースホルダが表示されるので、実行時間はあまり気にしなくてよさそう。
    • AppleWatchで操作しているとき
      • watchOS app→iPhone appは、Watch ConnectivityのWCSession.sendMessage(_:replyHandler:errorHandler:)で通知。
      • watchOS app→watchOS app complicationは、watchOS appからの通知に基づいてiPhone appが更新するのを待って、iPhoneで操作しているのと同じ経路(iPhone app→watchOS app→watchOS app complication)で通知。
      • iPhone app→iPhone widgetも、watchOS appからの通知に基づいてiPhone appが更新するのを待って、iPhoneで操作しているのと同じ経路でiPhone appから通知。
  • 検証のポイント

    • FCMのペイロードに、iPhone識別子を付加できるか。できない場合、watch OS appからWatch ConnectivityでiPhone appにiPhone識別子の問い合わせができるか。※追記 FCMのペイロードに追加するよりも、最初からWatch ConnectivityのtransferUserInfo(_:)で送りつけて、サイレントリモート通知のハンドラでそれを拾えるか、を検証した方がよさそう。
    • watchOS app→iPhone appが、iPhone appの状態によらずWatch Connectivityで通知できるか。

めちゃくちゃ複雑な構成になりそうです。
バックグラウンドのWatch Connectivityが3〜5分と言わず、もう少し速ければこんな複雑なことしなくて済むのですが。

kabeyakabeya

ふと、Watch Connectivityの受け側をcomplicationにしたらどうなるのか、という気がしましたね。
watchOS appでないと受けられない気がして試してませんでした。試してみます。


やっぱりダメでしたね。
何かエラーが出るというわけでもないのですが、受け取れませんでした。

kabeyakabeya

バックグラウンドのWatch Connectivityが3〜5分と言わず、もう少し速ければこんな複雑なことしなくて済むのですが。

実際のところ、FCM経由のサイレントリモートプッシュが3〜5分程度遅延するというのであれば、こんな複雑なことしても結局意味ない、という話になりかねないと思います。

kabeyakabeya

まだ悪あがき中です。

iPhone app側でローカル通知を出す場合、iPhoneがロックされていたりすると、AppleWatch側に通知が来ます。
iPhoneのローカル通知なのに。

なので、watchOS側でこれを捕まえられればよさそう、と思ってUNNotificationCenterのドキュメントを見ました。

https://developer.apple.com/documentation/usernotifications/handling_notifications_and_notification-related_actions

A silent notification (see Pushing background updates to your App).

とかって書いてあって「おお!」となりましたがリンク先はサイレントリモートプッシュ通知でした。
残念。

kabeyakabeya

FCMでメッセージを送信しようとしたとき、新しいFCMのAPI[1]ではOAuth2.0認証情報が必要と言うことなんですが、これWebサービスがない個別アプリから直接送信しようとするとちょっと無理なんじゃないかという気がしますね。

  1. GoogleのサーバでWebアプリがある場合は、そいつが持つ認証情報を使えます。
  2. Google以外のサーバでWebアプリがある場合は、FCMの秘密鍵をダウンロードして認証情報を生成し、それを使えます。
  3. そうでない場合は、(エンドユーザの)Googleアカウントを使用して有効期間の短いOAuth2.0アクセストークンを取得して利用できます。

これだけのためにGoogleアカウントを持ってない人にアカウントを取らせるというのもあれですし、またこれだけのためにWebアプリを作るというのもあれです。

どうしよっかな。


追記

そうでない場合は、(エンドユーザの)Googleアカウントを使用して有効期間の短いOAuth2.0アクセストークンを取得して利用できます。

これ違うような気がしてきました。もう少し調べます。


https://firebase.google.com/docs/cloud-messaging/migrate-v1?hl=ja#python_1

には、以下のように3つ(もしくはその組み合わせ)、とも読める書き方がしてあったのですが、先の2つのいずれかの認証情報を使って、OAuth2.0アクセストークンを生成する、というような感じではないかという気がしてきました。
Googleのドキュメントは割と分かりにくい箇所が多い気がします(パートによって突然分かりにくくなるイメージ)。翻訳の問題かと思って原文も見ましたが同じ感じですね。

サーバー環境の詳細に応じて、Firebase サービスへのサーバー リクエストを認可するために、これらの戦略の組み合わせを使用します。

  • Google アプリケーションのデフォルト認証情報(ADC)
  • サービス アカウントの JSON ファイル
  • サービス アカウントから派生した、有効期間が短い OAuth 2.0 アクセス トークン

ともかく、ユーザの認証情報ではなく、サービスアカウントの認証情報を渡す必要があります。

サービスアカウント用の秘密鍵JSONファイルをなにがしかの暗号化をしてiOSアプリに埋め込むというような荒技もあるような気はしますが、復号処理のコードも一緒に配布するわけなので、まあ良くないですね。
ニフクラのクライアントキーも同様の問題はありますが、Firebaseサービスアカウントの秘密鍵のほうが、漏れたときの影響が大きそうです。

脚注
  1. 古いAPIは、2024 年 2 月 23 日までに新しいAPIに移行しろ、ということらしいです ↩︎

kabeyakabeya

続いてニフクラモバイルバックエンドを調べています。

FCM同様、プッシュ通知ができるのですが、1点「配信端末アプリのBundleIDとAppleトピックIDが一致しない」とエラーになる、というような記述があります。
これはこれで困ります。配信用アプリと受信用アプリが違う、というケースに対応できません。
(今回のような、iPhoneアプリから、watchOSアプリに配信するようなケース)
ほんとかな。
いったんやってみます。


追記。ニフクラのドキュメントで「配信端末」と言っているのが、配信先端末、という意味合いのようですね。
ただプッシュ通知登録時にトピックIDを指定する方法がないので、起動時に登録するアプリケーションキーがトピックIDと紐付いている、ということでしょうか。
そうなると?

  • iPhone appからは、プッシュ通知を使用。watchOS appのトピックIDで送りたい→watchOS appのアプリケーションキーでの初期化が必要
  • watchOS appからは、Watch Connectivityを使用。

という感じになるので、watchOS appのアプリケーションキーを使用すればいいんでしょうか。大丈夫なのかな。
やってみます。


NCMB(Nifty Cloud MobileBackend)のライブラリはwatchOSには対応してませんでした。
なので?

  • watchOS app側では、NCMBの機能を利用せずに、deviceTokenを取得したらiPhoneにWatch Connectivityで通知。
  • iPhone app側で、watchOS app側のdeviceTokenをNCMBに登録
  • iPhone app自体は、NCMBに配信先として登録する必要がない

という感じでしょうか。
ややこしい。


watchOS app側のアプリケーションキーを使って、iPhone app側でdeviceTokenを保存しようとしても、エラー(E404001:No data available.)になってしまいます。

「配信端末アプリのBundleIDとAppleトピックIDが一致しない」とエラーになる

これですかね。やっぱりダメそうです。

kabeyakabeya

実際のところ、FCM経由のサイレントリモートプッシュが3〜5分程度遅延するというのであれば、こんな複雑なことしても結局意味ない、という話になりかねないと思います。

FCM経由ではなくクラウドキットのPush Notification Consoleからですが、watchOS appにType=Backgroundの通知(サイレントリモートプッシュ通知)を送ってみました。

動きとしては、appがフォアグラウンドにいればすぐにappに通知がくるけども、バックグランドの場合、OSで留め置きになってappがフォアグラウンドになるまで保留になる、というような動きをしています。

とはいえWatch Connectivity同様、バックグラウンドにいる間でも何かのタイミングで来ることがあります。
background notificationとは何?ってなります。感覚的には、Watch Connectivityとほぼ同等のような気がします。
(なので、この目的のためにリモートプッシュ通知を使うのは意味ない)

もう1個、watchOS appに対してType=complicationの通知も送ることができないのか試してみましたが、これは

と言われて送信できませんでした。topic=BundleIDなんですが、Type=Backgroundだとそのまま送れるので、メッセージが間違っているのか何なのか。


しかし、標準のリマインダー同様の「iPhoneで更新したらWatchのコンプリケーション表示も書き換える」がここまで難しいとは思いませんでした。

あとやるとしたら「WatchコンプリケーションのTimelineを短めに設定して、定期的に自分で(=コンプリケーションが)更新の有無をチェックする」かなと思いますが、バッテリーのことを考えるとしたくないですね。

なんかいい方法がないのかなと思います。

kabeyakabeya

あとついでにいくつか調べたことがあるので書いておきます。

watchOS appのコンプリケーションでは、@WKExtensionDelegateAdaptorが効きません

@main
struct MyWatchComplication: Widget {
    @WKExtensionDelegateAdaptor(MyDelegate.self) var delegate
    let kind: String = "MyWatchComplication"

    var body: some WidgetConfiguration {
    ...

上記のような感じで書けば使えそうなものですが、使えません。厳密に言うと、初期化はされるようですが、WKExtension.shared().delegateが設定されません。
delegateはread-onlyプロパティなので、これで設定されないとなるとどうしようもないです。

watchOS appのコンプリケーションでは、@WKApplicationDelegateAdaptorが効きません。

のような警告が出力されます。初期化はされるようですがWKApplication.shared().delegateが設定されません。
delegateはread-onlyプロパティなので、これで設定されないとなるとどうしようもないです。

PushKitは、結局よく分からず

PKPushType=.complicationが、WidgetKitベースのコンプリケーションで使えるのか調べようとしたのですが、結局pushRegistry(_:didUpdate:for:)が一度も呼び出されずでした。
そもそもType=complicationのプッシュ通知の送り方も分からない(Type=complicationにするとエラーになってしまう)。

https://www.kodeco.com/books/watchos-with-swiftui-by-tutorials/v1.0/chapters/10-keeping-complications-updated

上記の記事には、以下のような注意書きがあります。

Note: At the time of writing, watchOS has a bug — verified by Apple — that sometimes prevents your app from registering for PushKit notifications. Apple told me it believes it has determined the root cause. However, there’s no ETA on when the fix will be available.

「At the time of writing」がいつなのかはよく分かりませんが、iOS 13からうまく動かない、という話をちょくちょくみかけるので、もしかしたらもうかなり長いこと、こういう状態なのかも知れません。

つまり、WidgetKitベースのコンプリケーションではリモート通知を受信できない

可能性としてはPushKitでの受信が残されていますが、状況を踏まえると期待薄と言えます。

なので、仮にリモート通知を受信するのであれば、watchOS appのほうで受信して、reloadAllTimelinesなどでコンプリケーションをリロード、というような感じになるのではないかと思います。

kabeyakabeya

watchOS 9以降で、BackgroundTaskというのが使えるようになっていて、static var watchConnectivity: BackgroundTask<Void, Void>というのが「A background task used to receive background updates from the Watch Connectivity framework.」ということのようです。

これを使えば、Watch Connectivtiyで送られてきたデータがOS任せの10分経ってもwatchOS appに届かない、というような状態から、バックグラウンドタスクとしてもう少し高頻度にチェックできるんじゃないかという気がしています。

ちょっとやってみます。


追記。
全然来ませんね。先に通常のWatch Connectivityの受信が来てしまう始末です。何かしくじっているのか?と言っても、やることなんかほとんどないと思うのですが。

@main
struct MyWatchApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .backgroundTask(.watchConnectivity) {
            print("watchConnectivity")
            await updateData()
        }
        .backgroundTask(.appRefresh("appRefresh")) {
            print("appRefresh")
            await updateData()
        }
    }
    func updateData() async {
        print("updateData")
    }
}

.appRefreshのほうがより頻度高く来るのかと思いきや、そうでもないです。
(というか.appRefreshのほうは一度も来てない…)

kabeyakabeya

ドキュメントをよくよく見ると、なんか思ってたのと違いましたね…

https://developer.apple.com/documentation/watchkit/background_execution/using_background_tasks

Communicate using Watch Connectivity

The system can wake your app in the background to handle communication using Watch Connectivity. It calls your background task handler and then calls the relevant methods on your WCSessionDelegate.

When using a WKWatchConnectivityRefreshBackgroundTask task, you need to defer calling setTaskCompletedWithSnapshot(_:) until after you finish handling the call to the session delegate. For example, to handle a basic download task, use the following steps:

  1. The system calls your delegate’s handle(_:) method, passing a WKWatchConnectivityRefreshBackgroundTask task. In this method, your app needs to save the task so you can access it later.
  2. Then the system calls methods on your WCSessionDelegate, based on the type of data that the paired iPhone sends. Your app processes the incoming data in these delegate methods.
  3. Your app can use the current session’s hasContentPending method to determine whether you still have any pending data.
  4. After you’ve processed all the incoming data, call the setTaskCompletedWithSnapshot(_:) method on the background task you saved in step 1.

結局、Watch ConnectivityのWCSessionDelegate.didReceiveUserInfoとかが呼ばれる程度でしか、バックグラウンドタスク(.watchConnectivityとか、WKWatchConnectivityRefreshBackgroundTask)も呼ばれない、ということのようですね。しかも動作を見てみると、WCSessionDelegate.didReceiveUserInfoが呼ばれても、バックグラウンドタスクのハンドラは呼ばれない、というケースが結構あります。逆はなさそうに見えます。

なんのためにわざわざ分けてるのかよく分かりません。WCSessionDelegate.didReceiveUserInfoのなかで全部やればいいんじゃないのという気がしてしまいますが、もしかしたら何か理由があるのかどうなのか。

あとscheduleBackgroundRefresh(withPreferredDate:userInfo:scheduledCompletion:)も試してみましたが、目に見えて頻度があがるようなことはありませんでした。10秒後、とかに指定しても全然10秒後に来ません。

この流れだと、Timelineを短くしても全然その通りに呼ばれないような気がしてきました。

しかし、以下のドキュメントを最新化して欲しいですね。
古いコードのマイグレーションの話もまあ必要なんですが、最新の「こうすべき」というのをちゃんと書いて欲しい。

https://developer.apple.com/documentation/clockkit/deprecated_articles_and_symbols/keeping_your_complications_up_to_date

kabeyakabeya

今回、watchOSを色々調べてみて思ったのは、資源管理がかなり厳しくされていて安心?感心?する一方で、この資源割当がかなり一方的で押しつけがましいというところですね。

通知50件しか出せないとか、標準のリマインダーのバックグラウンド更新頻度は高いのに、デベロッパーのappの更新頻度は上げられないとか(使用頻度次第で上がっていくのかも知れませんが)。もっと自由度が上がれば、appとユーザの組み合わせによっては利便性が劇的に高まる可能性があるのに…と思ってしまいます。

資源割当は難しい問題なのでうかつにユーザに調整させるととんでもないことが起こる可能性はあるものの、開発者にしてみると、今の状態はちょっとどうなの?って感じてしまいます。