[SwiftUI]Apple Watchで画面遷移から戻った際にアニメーションが崩壊するのを防ぐ
WatchKitではアニメーションするのにパラパラ漫画のような画像を用意しなきゃいけなかったのが、SwiftUIを使うと簡単にApple Watchでもアニメーションをさせることができるようになりました。
ただ、時間の長いアニメーションをさせているときに画面遷移して戻ると崩壊してしまったので、その対処方法です。
サンプル
とりあえず以下のサンプルコードをApple Watchで実行してみてください。
struct ContentView: View {
@State var duration: TimeInterval = 30.0
@State var endDate: Date?
@State var value: TimeInterval = 0.0
@State var showModal = false
var body: some View {
VStack {
if let date = endDate {
Text(date, style: .timer)
} else {
Text(String(format: "%.0f:%02d", duration / 60, Int(duration) % 60))
}
GeometryReader { geometry in
Rectangle()
.foregroundColor(.gray)
Rectangle()
.foregroundColor(.red)
.frame(width: geometry.size.width * CGFloat(value))
}
.frame(height: 2)
.onAppear {
if let remain = endDate?.timeIntervalSinceNow {
value = remain / duration
withAnimation(.linear(duration: remain)) {
value = 0
}
} else {
value = 0
}
}
.id(endDate)
HStack {
if endDate == nil {
Button("Start") {
endDate = Date(timeIntervalSinceNow: duration)
}
.foregroundColor(Color.green)
} else {
Button("Stop") {
endDate = nil
}
.foregroundColor(Color.red)
}
Button("Modal") {
showModal = true
}
}
}
.padding()
.sheet(isPresented: $showModal) {
Button("Close") {
showModal = false
}
}
}
}
これ、iPhoneでは正常に動くのですが、現状手元でテストしているwatchOS 7.2だと画面遷移から戻った際に画像のようにアニメーションがおかしくなります。
対処方法
struct ContentView: View {
@State var duration: TimeInterval = 30.0
@State var endDate: Date?
@State var value: TimeInterval = 0.0
@State var showModal = false
@State var animationSession = 0.0
var body: some View {
VStack {
if let date = endDate {
Text(date, style: .timer)
} else {
Text(String(format: "%.0f:%02d", duration / 60, Int(duration) % 60))
}
GeometryReader { geometry in
Rectangle()
.foregroundColor(.gray)
Rectangle()
.foregroundColor(.red)
.frame(width: geometry.size.width * CGFloat(value))
}
.frame(height: 2)
.onAppear {
if let remain = endDate?.timeIntervalSinceNow {
value = remain / duration
withAnimation(.linear(duration: remain)) {
value = 0
}
} else {
value = 0
}
}
.onDisappear {
animationSession = floor(Date().timeIntervalSince1970 * 10)
}
.id(endDate)
.id(animationSession)
HStack {
if endDate == nil {
Button("Start") {
endDate = Date(timeIntervalSinceNow: duration)
}
.foregroundColor(Color.green)
} else {
Button("Stop") {
endDate = nil
}
.foregroundColor(Color.red)
}
Button("Modal") {
showModal = true
}
}
}
.padding()
.sheet(isPresented: $showModal) {
Button("Close") {
showModal = false
}
}
}
}
そこで、上記のコードのように、animationSessionという変数を作ってidに指定し、それをonDisappearで更新することで、画面遷移から戻った際に強制的にViewが再生成されるようにします。
ちなみにUUIDなどの実行時に毎回値が変わるものをidにしてしまうと、onAppearでアニメーションの更新をしている関係で無限ループに陥ります。それを回避するために、floor(Date().timeIntervalSince1970 * 10)
を使うことで0.1秒以内で更新が走った時は同じidを吐くようにして、無限ループを阻止しています。
あまりいいやり方ではないですが、困った時は参考にしてみてください。一番いいのは将来のwatchOSのアップデートで改善されることなのですがね。
コメント