SH Lab の アプリ開発部屋

リリースしたアプリの告知とかサポートとか、技術的なお話とか、そんな感じでつぶやきます。

SwiftUI 2.0で matchedGeometryEffect を使ってみる

SwiftUI 2.0で追加されたAPI

SwiftUI 2.0で追加された新機能のうち、Heroアニメーションを簡単に作れるAPIがあったのでちょっと触ってみました。

とりあえず完成形

こんな感じのSegmentControlっぽいUIを作ってみます。

https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7478/bf3ebbd6-0ab7-922c-1532-cd1b86dc1333.gif

Githubはこちらです。 github.com

開発環境

ボタンを作る

選択に使うボタンのViewを作ります。 そのまえに、適当にenumを定義しておきました。SF Symbolsから、適当に4つほど選出しています。

enum ButtonType: String, CaseIterable {
    case share = "square.and.arrow.up"
    case trash = "trash"
    case folder = "folder"
    case person = "person"
}

ボタンのビューはこんな感じで作ります。
AccentColorについては、適宜Assetsで好きな色を定義してください。

struct CustomButton: View {
    // 選択状態を表すプロパティ.
    @Binding var selected: ButtonType
    // 自分自身のボタンタイプ.
    let type: ButtonType
    
    var body: some View {
        ZStack {
            // 選択中だったら背景に円を描画する.
            if selected == type {
                Circle()
                    // AccentColorはAssetsで定義すること.
                    .fill(Color.accentColor) 
            }
            
            Button(action: {
                // ボタンをタップしたら選択状態を切り替える.
                selected = type
            }, label: {
                // enumから画像を表示する.
                Image(systemName: type.rawValue)
                    .resizable()
                    .renderingMode(.original)
                    .aspectRatio(contentMode: .fit)
                    .frame(width: 44, height: 44)
            })
        }
        .frame(width: 80, height: 80)
    }
}

画面上にボタンを並べる

では、画面上にボタンを並べてみます

struct ContentView: View {
    
    @State private var selected = ButtonType.share // 選択状態の初期値.
    
    var body: some View {
        HStack {
            // enumをforeachで回して、CustomButtonを横に並べる.
            ForEach(ButtonType.allCases, id: \.self) { type in
                CustomButton(selected: $selected, type: type)
            }
        }
    }
}

では動かしてみます。 0002.gif

選択状態を切り替えて、見た目も変わるようになりましたね。では、ここからアニメーションを追加していきましょう。

アニメーションさせる

まずはボタンの選択時の状態変化がアニメーションを伴うように、ボタンタップ時の挙動を一部修正します。

// 一部抜粋.
            
Button(action: {
    // アニメーションを伴わせる
    withAnimation {
        selected = type
    }
}, label: {
    // 省略
})

.matchedGeometryEffectを設定する

アニメーションさせたいViewに対して.matchedGeometryEffectを指定します。 これは、識別子とNamespaceを与えて、同期したいアニメーションをグルーピングする感じです。

まずはnamespaceを宣言します。

struct CustomButton: View {
    // 省略

    var namespace: Namespace.ID // namespaceを追加する.
    
    // 省略
}

次に、アニメーションさせたい背景ビューに対して.matchedGeometryEffectを指定します。識別子は、アニメーションを同期したいグループ間で一致していれば何でもいいです。

// 選択中だったら背景に円を描画する.
if selected == type {
    Circle()
        .fill(Color.accentColor)
        .matchedGeometryEffect(id: "CustomButton", in: namespace)
}

次に、呼び出しているView側に修正を加えます

struct ContentView: View {
    
    @State private var selected = ButtonType.share

    // 追加する.
    @Namespace var namespace

    var body: some View {
        HStack {
            ForEach(ButtonType.allCases, id: \.self) { type in
                // 引数にnamespaceを与えるように修正する.
                CustomButton(
                    selected: $selected,
                    type: type,
                    namespace: namespace
                )
            }
        }
    }
}

以上で完成です! とても簡単にできちゃいますね!

まとめ

Heroアニメーションはテンション上がるので、他にも色々と試して記事にしたいと思います!

こちらの書籍はとても参考になったのでおすすめです!

基礎から学ぶ SwiftUI

基礎から学ぶ SwiftUI