本記事は ZOZO Advent Calendar 2021 その1の14日目の記事です。
はじめに
横スクロールするとズームイン・ズームアウト表示をSwiftUIで実装してみたので紹介したいと思います。
ズームイン・ズームアウト |
---|
SwiftUIでどう実装するのか?
スクロールに合わせてコンテンツのスケールを変える実装するとしたら、例えばUICollectionViewではUICollectionViewLayoutのサブクラスを作成してlayoutAttributesForElementsをオーバーライドしてズームイン・ズームアウトのスケール調整したUICollectionViewLayoutAttributesを返すなど思いつくと思います。
レイアウトクラスをわざわざ作成するのは少々面倒ですね。
UICollectionViewを使わない場合はどのコンテンツをズームイン・ズームアウトさせるか管理するのが大変そうです。
class AnimatedCollectionViewLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
// ズームイン・ズームアウトを調整したUICollectionViewLayoutAttributesを返す
}
}
それがSwiftUIではサブクラスを作ることなく、GeometryReader
とscaleEffect(_:anchor:)
で、スクロールに合わせてスケールを変えることができます。
ScrollView(.horizontal) {
// view ...
GeometryReader { geometryReader in
let scale = ...
VStack {
// view ..
}
.scaleEffect(x: scale, y: scale, anchor: .bottom)
}
}
実装
scaleEffect(_:anchor:)
を使うために、ズームイン・ズームアウトのスケールを求めます。
スケールは以下の図のように変化をします。
赤いコンテンツのスケールの変化 スケール範囲(0 ~ 1) |
---|
スケールを求めるためには、スクロール量が必要になり、スクロール量はスクロールした赤のコンテンツの位置と赤のコンテンツを表示しているコンテンツ(親コンテンツ)の横幅が必要になります。
スクロールしたコンテンツの位置の取得
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)
}
}
まとめ
GeometryReader
とscaleEffect
のおかげで、スケールの変化だけを考えればよく、少ないコード量で実装できるのはとてもいいなと思いました。
またscale以外にも、opacityやtransformなども変えることができるので、色々と試して見たいと思います。