SwiftUI GeometryReader: Parallax Scrolling - Part 3

Mark Moeykens
Nov 16, 2019

This is part 3 in the GeometryReader Series. Part 2 is here.

In this article, you will learn a way to create a parallax effect when vertically scrolling. In order to do this, you want the background image to scroll more slowly than the rest of the content. Here is what you will build:



Get the Image Assets

You can definitely use your own image assets to build this screen. If you want to use the same images I used (National Parks in Utah) then go to my GitHub repo where I added the images.

Add these images to your Assets.xcassets in your new Xcode project.

Setup the Project

1. Create A New SwiftUI File
In your SwiftUI project, add a new SwiftUI file.

2. Setup the Background
You'll need a scroll view since you'll be scrolling the content. But notice the background image you're using is INSIDE the scroll view.

You want the background to scroll.

struct ParallaxScrolling: View { 
    var body: some View { 
        ScrollView { 
            ZStack { 
                    Image("map") 
                        .resizable() 
                        .aspectRatio(contentMode: .fill) 
                        .blur(radius: 1) 
                        .scaleEffect(1.8) 
                        .opacity(0.4) 
            }.edgesIgnoringSafeArea(.vertical) 
        } 
    } 
}

Other things to note:
  • The opacity and blur modifiers are used to lessen attention. You don't want your background competing for attention with your main content.
  • The scaleEffect was used to make the image bigger because I was too lazy to find a bigger image. 😃 (The correct thing to do would be to use a bigger image.)

Preview your view (or run the project) and it should look like this:

(That's actually a map of Utah.)

3. Build the Tiles
Let's create the image tiles for the national parks in Utah.
 
Image("Arches") 
    .resizable() 
    .aspectRatio(contentMode: .fill) 
    .frame(height: 200, alignment: .bottom) 
    .cornerRadius(20) 
    .shadow(color: .gray, radius: 10, x: 0, y: 5) 
    .overlay(VStack { 
        Spacer() 
        Text("Arches") 
            .padding(.bottom, 20) 
            .opacity(0.85) 
            .font(.system(size: 30, weight: .black)) 
            .foregroundColor(.white) 
    }) 

It's just an image with a text view overlayed on top of it.

Now you could just copy and paste this in 5 times but that creates a lot of duplicate code. Meaning, if you want to make a change, you have to change it in many places. And as you already saw above I'm kind of lazy. 😃

What you should do instead is to create another view (struct) for the image tile that you can reuse!

Create a new SwiftUI file for the tile. Here is the code I have:
 
struct Tile: View { 
    var imageName = "" 
    var tileLabel = "" 

    var body: some View { 
        Image(imageName) 
            .resizable() 
            .aspectRatio(contentMode: .fill) 
            .frame(height: 200, alignment: .bottom) 
            .cornerRadius(20) 
            .shadow(color: .gray, radius: 10, x: 0, y: 5) 
            .overlay(VStack { 
                Spacer() 
                Text(tileLabel) 
                    .padding(.bottom, 20) 
                    .opacity(0.85) 
                    .font(.system(size: 30, weight: .black)) 
                    .foregroundColor(.white) 
            }) 
    } 

 
struct Tile_Previews: PreviewProvider { 
    static var previews: some View { 
        Tile(imageName: "Arches", tileLabel: "Arches") 
    } 

You can see in the Preview code how you will be initializing this tile by passing in the image name and title text that will be overlayed on the image.

The preview should look similar to this:

(By the way, that's the famous "Delicate Arch" in Arches National Park in Utah. Fun Fact: That's where I proposed marriage to my wife. 💍)

4. Add the Tiles and Title
Great, you have your tile view ready. Now let's create more tiles for the national parks.

Since you're using a ZStack, you want to add the title and image tiles of the national parks on TOP of the map. So you will be adding a VStack as the next view in your ZStack.

struct ParallaxScrolling: View {
    var body: some View {
        ScrollView {
            ZStack {
                Image("map")
                    .resizable()
                    .aspectRatio(contentMode: .fill)
                    .blur(radius: 1)
                    .scaleEffect(1.8)
                    .opacity(0.4)

                VStack(spacing: 40) {
                    Text("UTAH")
                        .font(.system(size: 30, weight: .black))
                    Tile(imageName: "Arches", tileLabel: "Arches")
                    Tile(imageName: "Canyonlands", tileLabel: "Canyonlands")
                    Tile(imageName: "BryceCanyon", tileLabel: "Bryce Canyon")
                    Tile(imageName: "GoblinValley", tileLabel: "Goblin Valley")
                    Tile(imageName: "Zion", tileLabel: "Zion")
                }
                .padding(.horizontal, 40)
            }.edgesIgnoringSafeArea(.vertical)
        }
     }
}

Depending on the width of your background image, your tiles may seem too wide. The tiles are just stretching out to fit within the width of the background image.

Don't worry about this right now.

You will fix it soon. Your app should now look like this:

Notice the background map is moving at the same speed as the tiles.


Create the Parallax Effect with Help from the GeometryReader

In Part 2, you saw how the geometry reader can track the current location of a view while it is being scrolled:


You will be using this geometry data with the background image.

The Basic Concept
To create a parallax effect, you need two layers moving at different speeds. You can either 
  • Speed up the tiles or 
  • Slow down the background

In this example, you're going with the second option; slow down the background. How do you slow down the scroll speed of the background?

Offset to the Rescue
The offset modifier is used to move a view using an X and Y coordinate. In the book SwiftUI Views, there is a section on using the offset modifier:

When you scroll a view, it's offset is changing.
  • Scroll up 100 points, the Y offset moves to -100.
  • Scroll down 100 points, the Y offset moves to 100.

What you're going to do is change that Y offset to slow it down
For example:
  • Scroll up 100 points, you change the Y offset to move only -50.
  • Scroll down 100 points, you change the Y offset to move only 50.

So you need the GeometryReader to get the Y position and then subtract points.



How will you do this in code?

SwiftUI makes it super easy with two steps:
  1. You wrap the background image in a GeometryReader
  2. Apply an offset modifier to the background image that observes the Y position and then divides it by two (or really any number you want to get your desired effect).

Here are the two steps applied in code:

struct ParallaxScrolling: View {
    var body: some View {
         ScrollView {
             ZStack {
                 GeometryReader { gr in
                     Image("map")
                         .resizable()
                         .aspectRatio(contentMode: .fill)
                         .blur(radius: 1)
                         .scaleEffect(1.8)
                         .opacity(0.4)
                         // Subtract half the offset
                         .offset(y: -gr.frame(in: .global).origin.y / 2)
                 }
 
                 VStack(spacing: 40) {
                     Text("UTAH")
                         .font(.system(size: 30, weight: .black))
                     Tile(imageName: "Arches", tileLabel: "Arches")
                     Tile(imageName: "Canyonlands", tileLabel: "Canyonlands")
                     Tile(imageName: "BryceCanyon", tileLabel: "Bryce Canyon")
                     Tile(imageName: "GoblinValley", tileLabel: "Goblin Valley")
                     Tile(imageName: "Zion", tileLabel: "Zion")
                 }
                 .padding(.horizontal, 40)
             }.edgesIgnoringSafeArea(.vertical)
         }
     }
}

Don't forget the minus sign in there. If you don't use it, your background will move FASTER than the tiles (which is also a cool effect).

Also, remember, instead of .origin.y you can also use .minX to get the same value.

Summary

You just learned a really cool effect with the use of the:
  • ZStack (Depth Stack)
  • GeometryReader
  • Offset Modifier

Congratulations! 👏 🎉

The fun doesn't stop there though. Now that you understand this concept of being able to adjust the offset of a view in response to scrolling, there are more cool things you can do with the assistance of the GeometryReader.

Next

The GeometryReader can also be used to get the size of views and with that, you can adjust the size of views in response to scrolling. Coming up next, you will learn more about how to get the size of views using the GeometryReader and how you can apply that.

Continue to Part 4!

Free SwiftUI Picture Book

Screenshots for every code example? Yes! Get your FREE SwiftUI picture book here.

3 comments

Chris Parker
Nov 16, 2019
That's great Mark. Works a treat. One small question..... when you have your simulator set to Dark Mode, is there a way of applying a white background that overcomes the fact that the Dark Mode background is black and bleeds through?
Mark Moeykens
Nov 17, 2019
Yup, the color that ignores safe area is how I set background colors.