1
1

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 1 year has passed since last update.

ZOZOAdvent Calendar 2021

Day 14

SwiftUIでズームイン・アウトするスクロールトランジションの実装

Posted at

本記事は ZOZO Advent Calendar 2021 その1の14日目の記事です。

はじめに

横スクロールするとズームイン・ズームアウト表示をSwiftUIで実装してみたので紹介したいと思います。

ズームイン・ズームアウト
Dec-13-2021 11-04-10.gif

SwiftUIでどう実装するのか?

スクロールに合わせてコンテンツのスケールを変える実装するとしたら、例えばUICollectionViewではUICollectionViewLayoutのサブクラスを作成してlayoutAttributesForElementsをオーバーライドしてズームイン・ズームアウトのスケール調整したUICollectionViewLayoutAttributesを返すなど思いつくと思います。
レイアウトクラスをわざわざ作成するのは少々面倒ですね。
UICollectionViewを使わない場合はどのコンテンツをズームイン・ズームアウトさせるか管理するのが大変そうです。

class AnimatedCollectionViewLayout: UICollectionViewFlowLayout {
    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        // ズームイン・ズームアウトを調整したUICollectionViewLayoutAttributesを返す
    }
}

それがSwiftUIではサブクラスを作ることなく、GeometryReaderscaleEffect(_:anchor:)で、スクロールに合わせてスケールを変えることができます。

ScrollView(.horizontal) {
    // view ...

    GeometryReader { geometryReader in
        let scale = ...
        VStack {
            // view ..
        }
        .scaleEffect(x: scale, y: scale, anchor: .bottom)
    }
}

実装

scaleEffect(_:anchor:)を使うために、ズームイン・ズームアウトのスケールを求めます。

スケールは以下の図のように変化をします。

赤いコンテンツのスケールの変化
スケール範囲(0 ~ 1)
スクリーンショット 2021-12-13 22.08.44.png

スケールを求めるためには、スクロール量が必要になり、スクロール量はスクロールした赤のコンテンツの位置と赤のコンテンツを表示しているコンテンツ(親コンテンツ)の横幅が必要になります。

スクロールしたコンテンツの位置の取得
SwiftUIでスクロールしたコンテンツの位置を取得するために、ここでGeometryReaderを使用します。GeometryReaderについてはSwiftUIの肝となるGeometryReaderについて理解を深めるの記事がとてのわかりやすくまとめてくださっています。

UIScrollViewなどでは、contentOffsetからスクロールした位置を計算して求めていましたが、GeometryReaderを使用すると簡単に取得できて便利ですね。

ScrollView(.horizontal) {
    HStack(alignment: .center, spacing: 0) {
        ForEach(0..<colors.count) { num in
            GeometryReader { item in
                // スクロールしたコンテンツの位置を取得
                let itemFrame = item.frame(in: .global)
                 VStack { // view }
            }
        } 
    }
}

スケールの設定
コンテンツのクロール位置が取得できたら後は、スケールを設定するだけです。
以下がコードの全体像です。

struct ContentView: View {
    struct Page {
        var backgroundColor: Color
    }

    private let colors: [Color] = [.red, .orange, .yellow, .green, .mint, .blue, .purple]

    var body: some View {
        GeometryReader { mainView in
            let mainViewSize = mainView.frame(in: .global).size
            ScrollView(.horizontal) {
                HStack(alignment: .center, spacing: 0) {
                    ForEach(0..<colors.count) { num in
                        GeometryReader { item in
                            // スクロールによるズームイン・ズームアウトのスケールを計算
                            let scale = scale(mainFrame: mainView.frame(in: .global), itemFrame: item.frame(in: .global))
                            VStack {
                                EmptyView()
                            }
                            .frame(width: mainViewSize.width, height: mainViewSize.height)
                            .background(colors[num])
                            // コンテンツのスケールを変える
                            .scaleEffect(x: scale, y: scale, anchor: .bottom)
                        }
                        .frame(width: mainViewSize.width, height: mainViewSize.height)
                    }
                }
            }
        }
    }

    func scale(mainFrame: CGRect, itemFrame: CGRect) -> CGFloat {
        let scrollRate = itemFrame.minX / mainFrame.width
        let scale = scrollRate + 1
        return min(max(0, scale), 1)
    }
}

まとめ

GeometryReaderscaleEffectのおかげで、スケールの変化だけを考えればよく、少ないコード量で実装できるのはとてもいいなと思いました。
またscale以外にも、opacityやtransformなども変えることができるので、色々と試して見たいと思います。

1
1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?