Ideal Reality

興味の赴くままに

[SwiftUI]Apple Watchで画面遷移から戻った際にアニメーションが崩壊するのを防ぐ

WatchKitではアニメーションするのにパラパラ漫画のような画像を用意しなきゃいけなかったのが、SwiftUIを使うと簡単にApple Watchでもアニメーションをさせることができるようになりました。

ただ、時間の長いアニメーションをさせているときに画面遷移して戻ると崩壊してしまったので、その対処方法です。

Contents
スポンサーリンク

サンプル

とりあえず以下のサンプルコードを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のアップデートで改善されることなのですがね。

スポンサーリンク

コメント

投稿されたコメントはありません

名前

メールアドレス(任意)

コメント

関連する投稿

[Swift]Dictionaryに順番を持たせたい

[SwiftUI]List内のButtonやLinkのデザインをNavigationLinkっぽくする方法

[SwiftUI]EnvironmentObjectをウィンドウごとに別々のインスタンスにしたい

iOS14のUIDatePickerのデザインをカスタマイズする

NO IMAGE

[SwiftUI]SwiftUI App Life Cycleに変更したアプリが、アップデートした端末で起動できない時の対処法

SwiftのDecimal(string:)がどれだけ使えるか試してみた