[WWDC2023] New Additions to ScrollView in iOS17 Part 2 (Scroll Transitions)

thompson-dean

Dean Thompson

Posted on June 30, 2023

[WWDC2023] New Additions to ScrollView in iOS17 Part 2 (Scroll Transitions)

SwiftUI

With the introduction of iOS 17, Apple has added a load of new ScrollView modifiers that promise to greatly enhance our app development experience. I tested out four modifiers in Part 1. This article will focus on the modifier scrollTransition(_:axis:transition:). Scroll transitions allow us to modify views transitioning in and out of the visible region within a ScrollView. Let's get into it and check them out in depth.

If you like this article feel free to like it or give me a follow.

Below we have a simple vertical ScrollView showing random pictures from Lorem Picsum.

screen1

The Code:

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")
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Let's examine 3 different ways we could add scroll transitions to modify views coming into the visible area of a ScrollView in the example above.

1.

This example demonstrates the simplest way to implement .scrollTransition(). Attach the code below to the ZStack.

.scrollTransition { view, phase in
    view
      .opacity(phase.isIdentity ? 1 : 0)
    }

Enter fullscreen mode Exit fullscreen mode

The scrollTransition modifier accepts a closure with two parameters. The first one being an EmptyVisualEffect, and the second being ScrollTransitionPhase. EmptyVisualEffect is simply the existing view without any effects. ScrollTransitionPhase is an enum that defines the value or state of a transition into the viewport of a ScrollView. In the above example we name our EmptyVisualEffect view, and our ScrollTransitionPhase phase.
In the screen capture below, the opacity of each view changes as it transitions onto the screen.

Example:
screen2

To change the opacity of our view as it comes onto the screen, add the opacity modifier to the view. Then, use .isIdentity of our phase to track whether it is on the screen or not(it uses a Bool, so we can use a ternary operator). If it is on the screen, it will have an opacity of 1 (full opacity) and if not it will be 0.

Here is the full code.

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")
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

2.

In the second example we use very similar code except this time we will use the value of our phase which ranges between -1 and 1 to add scale effect to our view. Delete the scroll transition code from example 1 and add the following code to the ZStack.

.scrollTransition { view, phase in
    view
        .scaleEffect(1 - (phase.value < 0 ? -phase.value : phase.value))
    }     
Enter fullscreen mode Exit fullscreen mode

Here, we add the .scaleEffect() modifier to the view. We use the value of the phase to determine the scale effect of each view. When the value is less than zero, reverse the phase value so it works the same as when the value is over 0. If you only input .scaleEffect(1 - phase.value) the views at the bottom will come in double the size. Make sure to deal with negative phase values accordingly.

Here's what it looks like!
screen3

Code for example 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")
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

3.

In example 3, I will show you a couple more things we can do with the scrollTransition modifier.

Have a look at the following code:

.scrollTransition(topLeading: .identity,
                  bottomTrailing: .interactive
) { view, phase in
    view
    // manipulate view
}
Enter fullscreen mode Exit fullscreen mode

We can also configure our transitions for the topLeading (top for vertical and leading for horizontal) and the bottomTrailing (bottom for vertical and trailing for horizontal). These configurations use ScrollTransitionConfiguration. You have three options: 1. .animated, 2. .interactive, and 3. .identity. .animated adds a default animation to the view as it interpolates through phases. It can also be customized. Feel free to input your favorite animations. .interactive, as its name suggests, interactively interpolates the transition’s effect as the view appears on the screen. .identity adds nothing. Effects and animations will not be added to your view.

In my example below I have add a rotation effect to our scale effect in example 2. I made the topLeading configuration .identity so that it the view is not modified.

.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))
}
Enter fullscreen mode Exit fullscreen mode

And here is what it looks like!

screen4

Code for example 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")
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Thanks for checking out the post. If you didn't read Part 1 be sure to check it out.
All the best!

Dean Thompson

Follow me!
LinkedIn
Twitter
Instagram

References

💖 💪 🙅 🚩
thompson-dean
Dean Thompson

Posted on June 30, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related