[SwiftUI] Animation 向け Property Wrapper @AnimateState を作る

SwiftUI2021

     
⌛️ 2 min.
すこし実用的な Property Wrapper を作ってみます。

環境&対象

以下の環境で動作確認を行なっています。

  • macOS Monterey 12.3 RC
  • Xcode 13.3 RC
  • iOS 15.4RC

PropertyWrapper

今回は、PropertyWrapper を使っていきます。PropertyWrapper 自体については、以下の記事で説明しています。

Swift [Swift] PropertyWrapper 手を動かして理解する(その1 wrappedValue)
Swift [Swift] PropertyWrapper 手を動かして理解する(その2 wrappedValue の初期化)
[Swift] PropertyWrapper 手を動かして理解する(その3 $ もしくは projectedValue の理解)

ちなみに、今回は、projectedValue は、大きな役割は果たしません。

Animation

SwiftUI では、非常に簡単にアニメーションさせることができるようになっています。

プロパティを withAnimation 内で変更するとアニメーションになります。


//
//  ContentView.swift
//
//  Created by : Tomoaki Yagishita on 2022/03/10
//  © 2022  SmallDeskSoftware
//

import SwiftUI

struct ContentView: View {
    @State private var offset = false

    var body: some View {
        VStack {
            Button(action: {
                withAnimation {
                    offset.toggle()
                }
            }, label: {
                Text("Toggle offset")
            })
            Text("Hello, world!")
                .offset(y: offset ? 100 : 0)
        }
        .padding()
    }
}

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

上記のコードで、以下のようなアニメーションになります。

変数操作を withAnimation{ … } の中で行っていることでアニメーションが行われます。

withAnimation で行うことのできるアニメーションは、複数用意されています。

Apple のドキュメントは、こちら

withAnimation の引数に Animation を指定することで、指定したアニメーションにすることができます。

以下の例は、引数を Animation.easeInOut(duration: 2)を指定しています。


//
//  ContentView.swift
//
//  Created by : Tomoaki Yagishita on 2022/03/10
//  © 2022  SmallDeskSoftware
//

import SwiftUI

struct ContentView: View {
    @State private var offset = false

    var body: some View {
        VStack {
            Button(action: {
                withAnimation(.easeInOut(duration: 2) ){
                    offset.toggle()
                }
            }, label: {
                Text("Toggle offset")
            })
            Text("Hello, world!")
                .offset(y: offset ? 100 : 0)
        }
        .padding()
    }
}

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

Animationさせたいプロパティ

withAnimation は、非常に便利なのですが、もっと便利にしたくなりました。

アニメーションさせる時には、大抵 アニメーション対象とするプロパティが決まってきます。そのプロパティの変更を withAnimation で変更すれば良いのですが、プロパティが複数箇所で変更されると、withAnimation 指定する箇所が増えてきます。プロパティを変更する箇所が増えるに従って、withAnimation を付け忘れてしまったりしそうです。さらに、指定アニメーションを変更したくなった時には、アニメーションの変更忘れまで発生してしまいそうです。

複数箇所の withAnimation の変更管理をするのが疲れそうなので、逆転の発想で、「このプロパティは、常に withAnimatin で変更する & 常に 使用する animation は .easeInOut」のように変数定義の箇所で指定できれば、付け忘れや 変更忘れを気にしなくて良くなりそうです。

記事の最初で振り返った property wrapper は、このように 変数操作時に特定の手順を行うことを設定できる便利な仕組みです。

propertyWrapper AnimateState


@propertyWrapper
public struct AnimateState: DynamicProperty {
    let storage: State
    let animation: Animation?

    public init(wrappedValue: T, animation: Animation? = nil) {
        self.storage = State(initialValue: wrappedValue)
        self.animation = animation
    }
    
    public var wrappedValue: T {
        get {
            self.storage.wrappedValue
        }
        nonmutating set {
            withAnimation(animation) {
                self.storage.wrappedValue = newValue
            }
        }
    }
    
    public var projectedValue: Binding {
        storage.projectedValue
    }
}
MEMO

当初 @State と併用する @Animate も考えたのですが、使い道がないことに気づき、両方を合わせた @AnimateState という property wrapper にしました。

AnimateState 使用例

上で定義した AnimateState を使用すると、animation まで指定していたサンプルコードは、以下のようになります。


//
//  ContentView.swift
//
//  Created by : Tomoaki Yagishita on 2022/03/10
//  © 2022  SmallDeskSoftware
//

import SwiftUI

struct ContentView: View {
    @AnimateState(wrappedValue: false, animation: Animation.easeInOut(duration: 2))
    private var offset: Bool

    var body: some View {
        VStack {
            Button(action: {
                offset.toggle()
            }, label: {
                Text("Toggle offset")
            })
            Text("Hello, world!")
                .offset(y: offset ? 100 : 0)
        }
        .padding()
    }
}

@propertyWrapper
public struct AnimateState: DynamicProperty {
    let storage: State
    let animation: Animation?

    public init(wrappedValue: T, animation: Animation? = nil) {
        self.storage = State(initialValue: wrappedValue)
        self.animation = animation
    }
    
    public var wrappedValue: T {
        get {
            self.storage.wrappedValue
        }
        nonmutating set {
            withAnimation(animation) {
                self.storage.wrappedValue = newValue
            }
        }
    }
    
    public var projectedValue: Binding {
        storage.projectedValue
    }
}

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

上記の例では、プロパティの変更箇所は1か所ですが、複数に増えても withAnimation を付け忘れたりすることがなくなりますし、使用する Animation を都度 指定する必要ありません。

まとめ

SwiftUI で アニメーションをより手軽にするための Property Wrapper を作ってみました。

手軽にアニメーションするための property wrapper
  • withAnimation を使って、プロパティを変更するとアニメーションする
  • Animation の種類は複数用意されている
  • PropertyWrapper と withAnimation を組み合わせるとより手軽

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です