Scroll transition effects in iOS 17

Apple added scroll transition effects in iOS 17 that allow items behavior to be animated as they scroll into and out of view. There are a number of attributes that can be modified such as size, visibility as well as rotation of the object.



Card view to scroll

Start by defining a view to contain an image of an animal on a card. Load a group of images into resources for the App and label these from 'animal-01 to 'animal-25'. On the Card View, the images are set to resizable and scaledToFit, so the images are not distorted and a pale yellow border surrounds the pictures. Define a list of Animals that contains a list of identifiable Animal objects.


AnimalCardView

 1struct AnimalCardView: View {
 2    var animal: Animal
 3    
 4    var body: some View {
 5        ZStack {
 6            RoundedRectangle(cornerRadius: 40)
 7                .fill(.yellow.opacity(0.3))
 8                .frame(width: 320, height: 240)
 9                .shadow(color: .black, radius: 7, x: 8, y: 10)
10            Image(animal.name)
11                .resizable()
12                .scaledToFit()
13                .frame(width: 320, height: 240)
14                .clipShape(RoundedRectangle(cornerRadius: 40))
15                .overlay(RoundedRectangle(cornerRadius: 40)
16                    .strokeBorder(Color.yellow.opacity(0.4), lineWidth: 6))
17        }
18    }
19}

Animals

 1struct Animal: Identifiable {
 2    let id = UUID()
 3    let name: String
 4    
 5    init(num: Int) {
 6        self.name = "animal-\( String(repeating: "0", count: num < 10 ? 1 : 0) )\(num)"
 7    }
 8}
 9
10
11struct Animals {
12    var animalList: [Animal]
13    
14    init() {
15        animalList = []
16        for i in 1...25 {
17            animalList.append(Animal(num: i))
18        }
19    }
20    
21}

SwiftUI view to display an image in a card view



Scroll without any transition effect

Place the animal cards in a vertical stack in a scroll view to see the default scroll behavior.


NoTransitionView

 1struct NoTransitionView: View {
 2    let animals = Animals()
 3    
 4    var body: some View {
 5        VStack {
 6            HeaderView(heading: "No Transition")
 7
 8            ScrollView {
 9                LazyVStack {
10                    ForEach (animals.animalList) { animal in
11                        AnimalCardView(animal: animal)
12                    }
13                }
14            }
15        }
16    }
17}

Vertical stack of cards scrolling without any transition


Vertical stack of cards scrolling without transition



Scale Transition

Adding a scroll transition effect is achieved by adding a scrollTransition modifier to the Animal Card View. Scale effect can be set to some value less than 1 based on the ScrollTransitionPhase identity. The isIdentity transition phase is true when the view is in full view and false when the view is no longer in view. The transition is taken care of automatically where the scale begins to change as the view is scrolled out of view.


ScaleTransitionView

 1struct ScaleTransitionView: View {
 2    let animals = Animals()
 3
 4    var body: some View {
 5        VStack {
 6            HeaderView(heading: "Scale Transition")
 7            
 8            ScrollView {
 9                LazyVStack {
10                    ForEach (animals.animalList) { animal in
11                        AnimalCardView(animal: animal)
12                            .scrollTransition { content, phase in content
13                                    .scaleEffect(phase.isIdentity ? 1 : 0.6)
14                            }
15                    }
16                }
17            }
18        }
19    }
20}

Vertical stack of cards scrolling with a scale transition



Opacity Transition

The opacity of the content can be bound to the scroll transition phase identity so that the contents fade as the view is scrolled out of view. The degree to which the view fades can be set by specifying the opacity value when the phase isIdentity is false.


OpacityTransitionView

 1struct OpacityTransitionView: View {
 2    let animals = Animals()
 3
 4    var body: some View {
 5        VStack {
 6            HeaderView(heading: "Opacity Transition")
 7
 8            ScrollView {
 9                LazyVStack {
10                    ForEach (animals.animalList) { animal in
11                        AnimalCardView(animal: animal)
12                            .scrollTransition { content, phase in 
13                                content.opacity(phase.isIdentity ? 1 : 0)
14                            }
15                    }
16                }
17            }
18        }
19    }
20}

Vertical stack of cards scrolling with a opacity transition



Offset Transition

The x and y positioning can also be bound to the scroll transition phase identity so that the contents can slide in to place as the view is scrolled. The following code adjusts the x offset so that the cards seems to slide in and out from the trailing direction.


OffsetTransitionView

 1struct OffsetTransitionView: View {
 2    let animals = Animals()
 3    
 4    var body: some View {
 5        VStack {
 6            HeaderView(heading: "Offset Transition")
 7            
 8            ScrollView {
 9                LazyVStack {
10                    ForEach (animals.animalList) { animal in
11                        AnimalCardView(animal: animal)
12                            .scrollTransition { content, phase in 
13                                content.offset(x: phase.isIdentity ? 0 : 400)
14                            }
15                    }
16                }
17            }
18        }
19    }
20}

Vertical stack of cards scrolling with a offset transition



Scale and Opacity Transition

Multiple effects can be applied at the same time as the cards scroll into view. The example in the Apple documentation is to combine scaling with opacity, which creates the effect of the views appearing from the background and fading back into the background.


ScaleOpacityView

 1struct ScaleOpacityView: View {
 2    let animals = Animals()
 3
 4    var body: some View {
 5        VStack {
 6            HeaderView(heading: "Scale & Opacity Transition")
 7
 8            ScrollView {
 9                LazyVStack {
10                    ForEach (animals.animalList) { animal in
11                        AnimalCardView(animal: animal)
12                            .scrollTransition { content, phase in 
13                                content
14                                    .scaleEffect(phase.isIdentity ? 1 : 0.3)
15                                    .opacity(phase.isIdentity ? 1 : 0.3)
16                            }
17                    }
18                }
19            }
20        }
21    }
22}

Vertical stack of cards scrolling with combined scale and opacity transition



View Aligned

The viewAligned behavior can be used so that the scrolling always stops at the edge of a leading view. This works with scrollTargetLayout modifier to layout the LazyVStack container, that contain the main repeating content, within a ScrollView .


ViewAlignedView

 1struct ViewAlignedView: View {
 2    let animals = Animals()
 3
 4    var body: some View {
 5        VStack {
 6            HeaderView(heading: "Scale & Opacity Transition")
 7            Text("viewAligned")
 8
 9            ScrollView {
10                LazyVStack {
11                    ForEach (animals.animalList) { animal in
12                        AnimalCardView(animal: animal)
13                            .scrollTransition { content, phase in
14                                content
15                                    .scaleEffect(phase.isIdentity ? 1 : 0.3)
16                                    .opacity(phase.isIdentity ? 1 : 0.3)
17                            }
18                    }
19                }
20                .scrollTargetLayout()
21            }
22            .scrollTargetBehavior(.viewAligned)
23        }
24    }
25}

Vertical stack of cards scrolling with scrollTargetBehavior set to viewAligned


Vertical stack of cards scrolling with Scale and Opacity transition



Scale, Opacity, Rotation and Offset Transition

Where to stop with the effects on scroll transitions? The following combines scaling, opacity, rotation and offset to show the cards flying in from an angle and getting bigger and clearer and then flying out in the opposite direction. I think this might be a bit too much. The scroll transition phase value is used to determine the offset. This phase value is -1.0 when the view is in the topLeading phase, zero when in the identity phase and 1.0 when in the bottomTrailing phase.


ScaleOpacityRotationView

 1struct ScaleOpacityRotationView: View {
 2    let animals = Animals()
 3
 4    var body: some View {
 5        VStack {
 6            HeaderView(heading: "Scale, Opacity,")
 7            HeaderView(heading: "Rotation and Offset")
 8
 9            ScrollView {
10                LazyVStack {
11                    ForEach (animals.animalList) { animal in
12                        AnimalCardView(animal: animal)
13                            .scrollTransition(.interactive) { content, phase in 
14                                content
15                                    .scaleEffect(phase.isIdentity ? 1.0 : 0.2)
16                                    .opacity(phase.isIdentity ? 1 : 0.5)
17                                    .rotation3DEffect(
18                                        Angle.degrees(phase.isIdentity ? 0: 90),
19                                        axis: (x: 0.5, y: 0.0, z: 0.1))
20                                    .offset(x: phase.value * -200)
21                            }
22                    }
23                }
24                .scrollTargetLayout()
25            }
26            .scrollTargetBehavior(.viewAligned)
27        }
28    }
29}

Vertical stack of cards scrolling with combined scale, opacity, rotation and offset transition


Vertical stack of cards scrolling with Scale, Opacity, 3D Rotation and Offset in scroll transition



Conclusion

Scroll transition effects were added to SwiftUI in iOS 17. This allows views behavior to be animated as they scroll into and out of view. A common effect is to add a scale and opacity change so that the views appear to fade into the background. There are a number of behaviors that can be applied to the views as they scroll into and out of view, but adding too many effects can draw too much attention to the scrolling effect. This might distract from the actual view and have the user too focussed on the scrolling behavior. Having said that, I did find that once a scroll transition has been added, then a scroll without any transition seems somewhat broken or inferior.

The source code for ScrollEffectApp is available on GitHub.