14
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

matchedGeometryEffect と @Namespaceを使ったアニメーション

Last updated at Posted at 2020-10-18

SwiftUI2.0で追加されたAPI

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

とりあえず完成形

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

Githubはこちらです。
https://github.com/hoshi005/matched-geometry-animation

0001.gif

開発環境

  • Xcode 12.0.1
  • iOS 14.0.1

ボタンを作る

選択に使うボタンの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()
                    .fill(Color.accentColor) // AccentColorはAssetsで定義すること.
            }
            
            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_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            CustomButton(selected: .constant(.share), type: .share) // 選択状態.
            CustomButton(selected: .constant(.trash), type: .share) // 非選択状態.
        }
        .previewLayout(.fixed(width: 100, height: 100))
    }
}

スクリーンショット 2020-10-18 13.00.39.png

画面上にボタンを並べる

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

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)
            }
        }
    }
}

プレビューはこんな感じ

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            ContentView()
            Group {
                CustomButton(selected: .constant(.share), type: .share)
                CustomButton(selected: .constant(.trash), type: .share)
            }
            .previewLayout(.fixed(width: 100, height: 100))
        }
    }
}

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

アニメーションさせる

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


// 一部抜粋.
            
Button(action: {
    // ボタンタップ時の処理を、withAnimationメソッドのクロージャに渡す.
    withAnimation {
        selected = type
    }
}, label: {
    // enumから画像を表示する.
    Image(systemName: type.rawValue)
        .resizable()
        .renderingMode(.original)
        .aspectRatio(contentMode: .fit)
        .frame(width: 44, height: 44)
})

.matchedGeometryEffectを設定する

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

まずはnamespaceを宣言します。

struct CustomButton: View {
    // 省略

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

次に、アニメーションさせたい背景ビューに対して.matchedGeometryEffectを指定します。


// 選択中だったら背景に円を描画する.
if selected == type {
    Circle()
        .fill(Color.accentColor) // AccentColorはAssetsで定義すること.
        // 識別子は、アニメーションを同期したいグループ間で一致していれば何でもいいです.
        .matchedGeometryEffect(id: "CustomButton", in: namespace)
}

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

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

    // @Namespaceプロパティラッパーを使って、namespaceを宣言.
    @Namespace var namespace

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

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

プレビューは、こんな感じで修正しておけば動きます

struct ContentView_Previews: PreviewProvider {
    @Namespace static var namespace // static を忘れずに.
    static var previews: some View {
        Group {
            ContentView()
            Group {
                CustomButton(selected: .constant(.share), type: .share, namespace: namespace)
                CustomButton(selected: .constant(.trash), type: .share, namespace: namespace)
            }
            .previewLayout(.fixed(width: 100, height: 100))
        }
    }
}

まとめ

Heroアニメーションはテンション上がるので、他にも色々と試してみたいですね。

14
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?