[WWDC2023] iOS17におけるScrollViewの新機能 その2 (Scroll Transitions)

iOS 17では多くの新しいScrollViewモディファイヤが追加され、アプリ開発体験を大幅に向上させることを約束している。その1では4つのモディファイアを使ってみたが、この記事では、scrollTransition(_:axis:transition:)モディファイアに焦点を当てている。スクロールトランジションは、ScrollView内の表示領域に出入りするviewを変更することができる。それでは、詳しく見ていこう。
もしこの記事が気に入ったら、ぜひいいねやフォローをお願いします。
下記に示すのは、Lorem Picsumからランダムに画像を表示する簡単な縦ScrollViewの例です。

コード
struct ScrollExampleTransition: View {
var body: some View {
NavigationStack {
ScrollView(.vertical) {
LazyVStack(spacing: 16) {
ForEach(0..<50, id: \.self) { index in
ZStack {
RoundedRectangle(cornerRadius: 16)
.fill(.white.gradient)
VStack {
AsyncImage(url: URL(string: "https://picsum.photos/320/240")!) { image in
image
.resizable()
.aspectRatio(contentMode: .fit)
} placeholder: {
Spacer()
ProgressView()
Spacer()
}
.cornerRadius(8)
.padding(.top, 16)
Text("Photo \(index + 1)")
.font(.title)
.fontWeight(.bold)
.foregroundStyle(.black)
.padding(.bottom, 16)
}
}
.padding(.horizontal, 16)
.frame(height: 320)
.containerRelativeFrame(.horizontal)
}
}
}
.background(.black)
.navigationTitle("Scroll Transitions")
}
}
}
上記の例に対して、どのようにしてScrollViewのビジブルエリアに入るviewを変更するか、3つの異なる方法を紹介する。
1.
以下の例は、.scrollTransition()を最もシンプルに実装する方法を示している。以下のコードをZStackにつけてみてください。
.scrollTransition { view, phase in
view
.opacity(phase.isIdentity ? 1 : 0)
}
scrollTransitionモディファイヤは、二つのパラメーターを持つクロージャーを受け取る。1つ目はEmptyVisualEffectで、2つ目はScrollTransitionPhaseです。EmptyVisualEffectは、特に効果のない既存のviewを単純に示す。ScrollTransitionPhaseはScrollViewのビューポートへの遷移の値または状態を定義するenumです。上記の例では、EmptyVisualEffectをviewと呼び、ScrollTransitionPhaseをphaseと呼んでいる。
以下のスクリーンキャプチャでは、各viewが画面に出入りする時に不透明度が変化する。
例

画面に表示される時のviewの不透明度を変更するには、viewに不透明度モディファイヤを追加する。その後、.isIdentityを使用してフェーズが画面上にあるかどうかを追跡する(これはBoolを使用するので、三項演算子を使用できる)。画面上にある場合、不透明度は1(完全な不透明)となり、そうでない場合は0になる。
コード
struct ScrollExampleTransition: View {
var body: some View {
NavigationStack {
ScrollView(.vertical) {
LazyVStack(spacing: 16) {
ForEach(0..<50, id: \.self) { index in
ZStack {
// ...
}
.padding(.horizontal, 16)
.frame(height: 320)
.containerRelativeFrame(.horizontal)
.scrollTransition { view, phase in
view
.opacity(phase.isIdentity ? 1 : 0)
}
}
}
}
.background(.black)
.navigationTitle("Scroll Transitions")
}
}
}
2.
二つ目の例では、非常に似たコードを使用するが、今回はフェーズの値(-1から1の範囲)を使用してviewにscaleEffectを追加する。例1からスクロールトランジションコードを削除し、ZStackに以下のコードをつける。
.scrollTransition { view, phase in
view
.scaleEffect(1 - (phase.value < 0 ? -phase.value : phase.value))
}
ここでは、.scaleEffect()モディファイヤをviewに追加する。フェーズの値を使用して各viewのscaleEffectを決定する。値がゼロより小さい場合は、フェーズ値を反転させて、値が0より大きい場合と同じように動作するようにする。.scaleEffect(1 - phase.value)だけを入力すると、下部のviewが2倍のサイズで表示される。負のフェーズ値を適切に処理するように注意してください。
実装した結果

コード
struct ScrollExampleTransition: View {
var body: some View {
NavigationStack {
ScrollView(.vertical) {
LazyVStack(spacing: 16) {
ForEach(0..<50, id: \.self) { index in
ZStack {
// ...
}
.padding(.horizontal, 16)
.frame(height: 320)
.containerRelativeFrame(.horizontal)
.scrollTransition { view, phase in
view
.scaleEffect(1 - (phase.value < 0 ? -phase.value : phase.value))
}
}
}
}
.background(.black)
.navigationTitle("Scroll Transitions")
}
}
}
3.
例3では、scrollTransitionモディファイヤでできるいくつかのことをご紹介する。
以下のコードを見てください
.scrollTransition(topLeading: .identity,
bottomTrailing: .interactive
) { view, phase in
view
// manipulate view
}
topLeading(verticalの場合はtop、horizontalの場合はleading)とbottomTrailing(verticalの場合はbottom、horizontalの場合はtrailing)のトランジションも設定することができる。設定はScrollTransitionConfigurationを使用する。選択肢は3つあります:1. .animated、2. .interactive、3. .identity。.animatedは、viewがフェーズを補間するときにデフォルトのアニメーションを追加する。これもカスタマイズ可能です。お気に入りのアニメーションを入力してみてください。.interactiveは、viewが画面に表示されるときにトランジションの効果を対話的に補間する。.identityは何も追加しない。エフェクトやアニメーションはビューに追加されない。
以下の私の例では、例2のscaleEffectにrotationEffectを追加した。topLeadingの設定を.identityにして、ビューが変更されないようにした。
.scrollTransition(topLeading: .identity,
bottomTrailing: .interactive
) { view, phase in
view
.scaleEffect(1 - (phase.value < 0 ? -phase.value : phase.value))
.rotationEffect(phase.isIdentity ? .degrees(0) : .degrees(180))
}
こんな感じになる

3のコード
struct ScrollExampleTransition: View {
var body: some View {
NavigationStack {
ScrollView(.vertical) {
LazyVStack(spacing: 16) {
ForEach(0..<50, id: \.self) { index in
ZStack {
// ...
}
.padding(.horizontal, 16)
.frame(height: 320)
.containerRelativeFrame(.horizontal)
.scrollTransition(
topLeading: .identity,
bottomTrailing: .interactive
) { view, phase in
view
.scaleEffect(1 - (phase.value < 0 ? -phase.value : phase.value))
.rotationEffect(phase.isIdentity ? .degrees(0) : .degrees(180))
}
}
}
}
.background(.black)
.navigationTitle("Scroll Transitions")
}
}
}
ここまで読んでくれてありがとうございました。
Spacely, Inc. App Div.
Dean Thompson
Follow me!
Twitter
Discussion