In this code-along we will continue to extend our app we created in “ScrollViews Part 1”. We will create the parallax effect similar to the presentation at the WWDC24 (Create custom visual effects with SwiftUI)) and will also add our own spin to it.
At the end it will look the following:

Let’s get started!
Step 1: Open your previous project
If not yet open, open your project we created in the previous code-along “ScrollViews Part 1”.
Step 2: Create a parallax effect
Create a new SwiftUI file. Go to File - New - File from Template... or simply press cmd + N, choose iOS Template and here SwiftUI View, click Next, choose ParallaxView as the name of the new view, ensure that DifferentScrollViews is selected under Group, check the tick box for Targets: DifferentScrollViews, click Create.
You are already familiar how to define the animals – please insert before the body:
let animals = Animal.all
Next, we want to add a title. Therefore, In body we create a VStack with:
VStack {
Text("Parallax Effect")
.font(.title.bold())
}
Below this Text-View we add our well-known ScrollView, LazyHStack and ForEach-Loop which creates a regular scroll view:
ScrollView(.horizontal) {
LazyHStack(spacing: 18) {
ForEach(animals) { animal in
VStack {
AnimalPhoto(animal: animal)
Text(animal.imageName.capitalized)
.font(.title2)
}
}
}
}
.contentMargins(36)
.scrollTargetBehavior(.paging)
.frame(height: 350)
This code we discussed in the previous code-along. The new .contentMargins(36) modifier adds padding around the content inside the ScrollView. It applies a margin (spacing) of 36 points around the entire content inside the ScrollView. It affects the spacing between the edges of the ScrollView and its internal content (LazyHStack). It prevents images from being too close to the screen edges.
Then add the .scrollTranistion modifier to the AnimalPhoto:
AnimalPhoto(animal: animal)
.scrollTransition(axis: .horizontal) { content, phase in
content
.offset(x: phase.isIdentity ? 0 : phase.value * -200)
}
.scrollTransition(axis: .horizontal) modifies the position of the image dynamically based on the scroll progress. .scrollTransition() moves the images along the X-axis. The phase object contains phase.isIdentity which is true when the item is centered on the screen, and a phase.value which is between -1.0 and 1.0, depending on how far the item is from the center.
The .offset(x: phase.isIdentity ? 0 : phase.value * -200) creates an offset if the image is not in the center:
- When the item is centered (
phase.isIdentity == true): The offset is0, i.e. there is no shift of the image. - When the item moves left or right (
phase.value ≠ 0): The offset is calculated usingphase.value * -200: Moves left when scrolling right (phase.value > 0), moves right when scrolling left (phase.value < 0). The-200multiplier makes the offset stronger. This makes images move in the opposite direction of the scroll, creating a parallax effect.
To make the parallax effect work, we need to add two modifiers: containerRelativeFrame() and .clipShape():
AnimalPhoto(animal: animal)
.scrollTransition(axis: .horizontal) { content, phase in
content
.offset(x: phase.isIdentity ? 0 : phase.value * -200)
}
.containerRelativeFrame(.horizontal)
.clipShape(RoundedRectangle(cornerRadius: 12))
.padding(.bottom, 12)
.containerRelativeFrame(.horizontal) ensures that the item takes the full horizontal space inside the scroll view. It ensures that each AnimalPhoto view spans exactly the width of its container’s scroll space — like a carousel “page”.
The .clipShape() modifier clips the view to a specific shape, in this case, a rounded rectangle with a corner radius of 12. It applies a rounded rectangle mask and forces the view to only display inside this rounded rectangle. Any parts outside the shape are cut off (clipped). Without .clipShape()but with .containerRelativeFrame, the .scrollTransition offset moves the content beyond its frame — it literally slides part of it out of bounds. So you need .clipShape(...) to chop off those parts, or else the “sliding” part will visually overlap the neighboring item.The .clipShape()modifier ensures that only the visible part of each image remains within bounds.
Finally, we add the .scrollTransition() to the Text view as well so that it creates the parallax effect in sync with the image.
To double-check, the ScrollView should look like the following:
ScrollView(.horizontal) {
LazyHStack(spacing: 18) {
ForEach(animals) { animal in
VStack {
AnimalPhoto(animal: animal)
.scrollTransition(axis: .horizontal) { content, phase in
content
.offset(x: phase.isIdentity ? 0 : phase.value * -200)
}
.containerRelativeFrame(.horizontal)
.clipShape(RoundedRectangle(cornerRadius: 12))
.padding(.bottom, 12)
Text(animal.imageName.capitalized)
.font(.title2)
.scrollTransition(axis: .horizontal) { content, phase in
content
.opacity(phase.isIdentity ? 1 : 0)
.offset(x: phase.value * 100)
}
}
}
}
}
.contentMargins(36)
.scrollTargetBehavior(.paging)
.frame(height: 350)
We can make the parallax effect more pronounced so that it looks like the images are lying directly over each other:
GeometryReader { geometry in
let screenWidth = geometry.size.width
VStack {
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack(spacing: 0) {
ForEach(animals) { animal in
GeometryReader { proxy in
let scrollOffset = proxy.frame(in: .global).minX
VStack(spacing: 8) {
AnimalPhoto(animal: animal)
.frame(width: screenWidth)
.offset(x: -scrollOffset)
.containerRelativeFrame(.horizontal)
.clipShape(Rectangle())
Text(animal.imageName.capitalized)
.font(.title2)
.scrollTransition(axis: .horizontal) { content, phase in
content
.opacity(phase.isIdentity ? 1 : 0)
.offset(x: -scrollOffset)
}
}
}
.frame(width: screenWidth)
}
}
.scrollTargetLayout()
}
.scrollTargetBehavior(.paging)
}
}
If you wrap the above ScrollView and the GeometryReader in a VStack, then you can directly compare both views.
Step 3: Including the ParallaxView in our TabView
You have already seen in the first part of the ScrollView code-along how a TabViewworks.
In ContentView insert in the TabView closure below the SimpleTabView()closure:
ParallaxView()
.tabItem {
Image(systemName: "photo")
Text("Parallax")
}
You can of course use any other SF Symbol image. We have chosen the photo Image.
In our app, we got 3 tabs with all the views we created earlier.
Step 4: Run your app
Now that your app layout is polished, it’s time to see it in action!
You see already a preview of your app on the right-hand side in the canvas. To run your app in the simulator, please select a device at the very top-middle of the XCode window – you can also add your own physical device – and press the Play button.
Congratulations!
You’ve successfully built 3 different ScrollViews! 🎉
What you have learned
In this code-along, you’ve learned how to
- create a horizontal
ScrollViewusingLazyHStackandForEach. - apply a parallax effect using the
.scrollTransitionmodifier andphase.value. - use
.containerRelativeFrame(.horizontal)to align each item with the scroll container’s bounds. - use
.clipShape()to mask overflow caused by scroll transitions.
That’s a wrap!
Keep learning, keep building, and let your curiosity guide you. Happy coding! ✨
“Stay hungry. Stay foolish.” — Steve Jobs
Download the full project on GitHub: https://github.com/swiftandcurious/DifferentScrollViews
Watch the full code-along
You can watch the full code-along (part 1 and 2) on YouTube: https://youtu.be/VJLLLYyluxE

