【watchOS8】アプリの常時表示機能で文字盤を作る

**注: 自作文字版は審査に落ちます。自作文字版はあくまで遊びです。**

#前書き
初めまして! iOSアプリのエンジニアでVTuberになれるアプリを開発中の、蒼黯(あおぐろ)です。
https://twitter.com/pc_blue_screen
WWDC21、iOS15やwatchOS 8などが発表されましたね。
早速ですが、watchOS 8でアプリが常時表示できる新機能が発表されたので、試していきましょう。(本当はSharePlayやりたかったけど、署名サーバーが未対応だった...)
> Appleとの秘密保持契約に基づきベータ版のスクリーンショット投稿は行いません。
> 記事内容もAppleが一般向けに公開している資料に書かれてることを記事化しています。

#環境
* macOS BigSur 11.4
* Xcode 13 Beta 1
* iOS 15 Developer Beta 1
* watchOS 8 Beta

#常時表示
Apple Watch Series 5以降(SE除く)の常時表示がwatchOS 8でデベロッパに解放された。
音楽再生やワークアウトなどのセッション時に限られるものの、
###事前設定
常時表示を実現するために、まずはInfo.plistでWKSupportsAlwaysOnDisplayキーをtrueにする必要がある。
Info.plistにWKSupportsAlwaysOnDisplayと打つか、一覧からApp Supports Always On Display (Watch)を選択すれば出てくるので、YESを選択する。
###状態の判別
**@Environment(\.scenePhase) private var scenePhase** で判定できる。
戻り値はactive/inactive/backgroundの3つで、意味はそのまま。

```swift:
if scenePhase == .active {
        Text("通常表示")
    } else {
        Text("腕を下げた時")
}
```
※TimelineViewというViewでswitch分岐も作れるが、現時点ではバグで再現できていない。
###情報を隠す
手首を下げた時に表示するべきでないプライベートな情報は**.privacySensitive()** で隠す。
もしくは**@Environment(\.redactionReasons) var redactionReasons**を呼んでおき、

```swift:
if !redactionReasons.contains(.privacy) {
    Text("5000兆円欲しい")
}
```
のようにする。
#まとめ

```swift:
struct ContentView: View {
@Environment(\.scenePhase) private var scenePhase
@Environment(\.redactionReasons) var redactionReasons
    VStack {
        if scenePhase == .active {
            Text("腕を上げた時")
        } else {
            Text("腕を下げた時")
        }
        Text("Privacy")
            .privacySensitive()
    }
}
```
#文字版を作る
上の仕組みを悪用すれば、簡易的に文字版も作ることができます。
(フォントは廻想体お借りしました! https://moji-waku.com/kaiso/)

右上の時計表示はプライベートAPIじゃないと消せないっぽいです。

```swift:ContentView.swift
import SwiftUI

struct ContentView: View {
    @Environment(\.scenePhase) private var scenePhase
    @State var D = Calendar.current.component(.day, from: Date())
    @State var H = Calendar.current.component(.hour, from: Date())
    @State var M = Calendar.current.component(.minute, from: Date())
    @State var S = Calendar.current.component(.second, from: Date())
    let timer = Timer.publish(every: 1, on: .current, in: .common).autoconnect()
    var body: some View {
        VStack {
            ZStack{
                Circle()
                    .trim(from: 0, to: CGFloat(Double(self.H) / 30))
                    .stroke(Color.blue,style: StrokeStyle(lineWidth: 10, lineCap: .square, lineJoin: .bevel))
                    .rotationEffect(.degrees(-90))
                    .rotation3DEffect(Angle(degrees: 12), axis: (x: 0, y: 0, z: 0))
                    .frame(width: 120, height: 120)
                    .animation(Animation.linear, value: CGFloat(Double(self.H) / 30))
                Circle()
                    .trim(from: 0, to: CGFloat(Double(self.M) / 60.0))
                    .stroke(Color.red,style: StrokeStyle(lineWidth: 10, lineCap: .square, lineJoin: .bevel))
                    .rotationEffect(.degrees(-90))
                    .rotation3DEffect(Angle(degrees: 60), axis: (x: 0, y: 0, z: 0))
                    .frame(width: 100, height: 100)
                    .animation(Animation.linear, value: CGFloat(Double(self.M) / 60.0))
                
                if scenePhase == .active {
                    Circle()
                        .trim(from: 0, to: CGFloat(Double(self.S) / 60.0))
                        .stroke(Color.orange,style: StrokeStyle(lineWidth: 10, lineCap: .square, lineJoin: .bevel))
                        .rotationEffect(.degrees(-90))
                        .rotation3DEffect(Angle(degrees: 80), axis: (x: 0, y: 0, z: 0))
                        .frame(width: 80, height: 80)
                        .animation(Animation.linear, value: CGFloat(Double(self.S) / 60.0))
                }
                
                Text("\(self.D)")
                    .font(.custom("Kaisotai-Next-UP-B", size: 30))
            }
        }.onReceive(timer){ _ in
            self.H = Calendar.current.component(.hour, from: Date())
            self.M = Calendar.current.component(.minute, from: Date())
            self.S = Calendar.current.component(.second, from: Date())
        }
    }
}

struct ContentView_Previews: PreviewProvider  {
    static var previews: some View {
        ContentView()
    }
}
```

#宣伝
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">【更新】開発中のVTuberアプリについて<a href="https://twitter.com/hashtag/Vtuber?src=hash&amp;ref_src=twsrc%5Etfw">#Vtuber</a> <a href="https://twitter.com/hashtag/Vtuber%E3%81%95%E3%82%93%E3%81%A8%E7%B9%8B%E3%81%8C%E3%82%8A%E3%81%9F%E3%81%84?src=hash&amp;ref_src=twsrc%5Etfw">#Vtuberさんと繋がりたい</a> <a href="https://t.co/g9Q2IWMxQs">pic.twitter.com/g9Q2IWMxQs</a></p>&mdash; 蒼黯@VTuberアプリ開発中 (@pc_blue_screen) <a href="https://twitter.com/pc_blue_screen/status/1390869199157817348?ref_src=twsrc%5Etfw">May 8, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
#参考
Designing Your App for the Always On State - Apple Developer
https://developer.apple.com/documentation/watchkit/designing_your_app_for_the_always_on_state