🕚

AppleWatch: アナログ時計アプリを自作する

2023/06/07に公開4

※スクラップに書いていた内容を記事にまとめました。

ことの始まり

昔、たぶん2000年頃、新潮文庫にYONDA? CLUBというキャンペーンがあって、時計をもらいました。

電池を替えバンドを替え、かなり長いこと使ってましたが、5年ほど前に文字盤がひび割れてどうにもならなくなってしまっていました。

WatchAppを初めて作るのでそれを模してやってみよう、AppleWatchの時計はClockolgyというアプリで作るのが定番らしいのですが、まずはいっぺん自作でやってみよう、という話です。
もっと言うと、最初はFaceを作れるんじゃないかと調べましたがどうやらダメで、アプリとして作るしかない、ということでした。

サンプルプロジェクトをgithubに置きました。
(Xcode 14.3.1, watchOS 9.5.1)

SwiftUIだと時計の絵を描くのが簡単

Xcode 14でwatchOS向けAppのプロジェクトを作ると、問答無用でSwiftUIのプロジェクトが生成されます。

ContentViewに絵を描くだけです。楽勝です。
針は若干ややこしいですが、時刻から角度を計算して、.rotationEffectで回してやれば良いです。
ZStackで重ねると良いですね。
あとは、時刻が進むにつれて表示を更新するだけです。

時計アプリ開発上の課題

とは言え、時計アプリを作るにはいくつか課題があります。

  1. 定期的に表示を更新するものの、いつの間にか更新が止まる
  2. アプリが一定時間経つとバックグラウンドに行ってしまう
  3. 右上の時刻表示が消せない

順に解説します。

定期表示更新の問題

時計アプリなので、Timerで表示を更新しようとしますが、これが止まります。
割とすぐに止まります。時計が落ち着く?とやや暗くなりますが、そうなると止まります。

これを回避するにはTimelineViewというのの中に描きます。
このTimelineViewは、クロージャを定期的に呼び出します。contextに、何のタイミングで呼んでいるのかが示されているので、必要に応じてそれをチェックして描きます。
秒針まで描く場合はノーチェックで描いてもいいんじゃないかと思います。

これによって、ずっと止まらず更新されるようになります。
ただしやっぱりある程度経つと、15秒おきとかに更新頻度が落ちます。

これは次の問題にも関係します。

アプリが一定時間経つとバックグラウンドに行ってしまう問題

watchOS 9では、設定アプリの「時計に戻る」で最大1時間まで、アプリを前面に出しておけますが、それが過ぎるとフェイスに戻ってしまいます。
時計アプリとしてはそれでは困ります。

というわけで「時計に戻らせない」方法ですが、HealthKitでワークアウトセッションを開始すると竜頭を押すまでは時計に戻らない、ということを利用します。

色々試したところ、プロジェクト→ターゲットの設定でHealthKitのCapabilityを設定しておけば、HealthKitデータの読み書きをしないので特に承認を得る必要もなく、ワークアウトセッションを開始することができました。
(ただしwatchOS 9がそうというだけで、今後もそうなのかは分かりません)

ワークアウトセッションは、ContentView.onAppearで開始すればOKそうです。
ちなみにプレビューはこれをするとクラッシュしますので、プレビューかどうかを判断するコードを入れておくと良いです。

これをすると、時計アプリがずっと前面に居るようになります。他のアプリからの通知は表示されます。
通知が消えると、時計アプリに戻ります。
さきほど「いつのまにか15秒おきになってしまう」といっていた再描画も、ずっと描画されるようになります。

右上の時刻表示が消せない問題

Apple Watchなので時計が表示されるのは正しいのですが、時計アプリでは時計が表示されるのは困ります(?)

消すには、アンドキュメンティドなプライベートAPIを使います。
Objective-CのAPIなので、ブリッジヘッダ経由で呼ぶか、無理矢理呼びます。サンプルコードでは無理矢理Swiftから呼んでいます。集中モードなどのインジケータもこれで消せます。

ただ、たぶんこれをやってしまうと、App Storeでの審査に通らないのではないかと思います。
(案外通るのかも知れませんが)

このAPIそのものの解説はできないので、サンプルを見てもらうのがよいと思います。
また、Objective-CのAPIをブリッジヘッダなしで無理矢理呼ぶ方法は、また別途記事にしたいと思いますが、ここでは解説をやめておきます。
見てもらえればなんとなく分かるのではないかと思います。

サンプルプロジェクト

というわけで、できあがったプロジェクトをgithubに登録しました。
YONDAは著作権があるので配布はせず、ネコの絵を入れました。

https://github.com/takenori-kabeya/AnalogFace

アナログ時計のサンプルですが、絵を描いているだけなので、色々応用はできると思います。

参考

https://stackoverflow.com/questions/38067952/how-to-hide-or-remove-the-time-from-the-apple-watch-status-bar

https://stackoverflow.com/questions/40533914/is-there-any-alternative-for-nsinvocation-in-swift

https://developer.apple.com/forums/thread/3657

Discussion

kabeyakabeya

まだ詳しく見てませんが、watchOS 10では

HealthKitでワークアウトセッションを開始すると竜頭を押すまでは時計に戻らない

というのが効いてないような気がします。
(ワークアウトセッションを開始しても時計に戻ってしまう)

kabeyakabeya

効いてます(戻りません)ね…
たまたま竜頭押しちゃったとか画面を長押ししちゃったとかなのか、それともなにかをきっかけに解除されるのか…

あと、ワークアウトを開始すると、当然ですが「ヘルスケア」→「アクティビティ」に、その時間が加算されてしまいます。すぐ一時停止とかしたら良いのかな

kabeyakabeya

あと、ワークアウトを開始すると、当然ですが「ヘルスケア」→「アクティビティ」に、その時間が加算されてしまいます。すぐ一時停止とかしたら良いのかな

startActivityの直後にpauseしても前面に居続けますし、アクティビティにも加算されていないように見えます。
これでいいかも知れません。

kabeyakabeya

自前のAppで、HKWorkoutSessionを「前面に居続けさせるため」に使おうとして、AppStoreに提出したら却下されました。
HKWorkoutSessionは、ヘルスデータを記録するためのものだからダメ、だそうです。