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}
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}
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}
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}
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}
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}
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}
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}
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.