Stretchy Header and Parallax Scrolling in SwiftUI

Share on facebook
Share on twitter
Share on pinterest

Welcome! In this tutorial, we are going to create a sticky/stretchy header in SwiftUI. At the same time, we are learning how to equip certain views with a parallax scrolling effect. We are using those features to create a nice looking blog post screen as you see them in several news apps.

This is what we are going to achieve in this tutorial:


If not already done, create a new Xcode 11 project and choose Single View app. Make sure you’ve selected SwiftUI as the interface mode and create a new project. For the following steps, you can use the default ContentView.swift file.

We’ll use some images for our project, which you can download here and here. You can also usa a portrait image like this one. Note that the format and dimensions of the header image can affect the parallax effect, which you maybe already noticed in the preview video above.

Import the images into your Assets.xcassets folder and make sure you name them correctly.

Setting up the basic content layout 👨‍🎨

Before we get started with implementing our sticky header, we are going to design the basic layout of our blog post screen.

The whole content in our ContentView should be scrollable. For this purpose, we replace the default “Hello World” Text view with a ScrollView.

struct ContentView: View {
var body: some View {
        ScrollView {
        }
    }
}

Inside the ScrollView, we will place the headline and the content texts of our blog post screen. All of those objects should be aligned vertically. Therefore, we insert a VStack into our ScrollView and choose .leading as the alignment mode.

ScrollView {
    VStack(alignment: .leading) {

    }
}

Above the headline of our article, we want to contribute the author by naming him and showing his picture. To do this, we insert a HStack into our VStack. We fill this HStack with a small, rounded image and another VStack containing the author’s name as a Text.

VStack(alignment: .leading) {
                HStack {
                    Image("journalist")
                        .resizable()
                        .aspectRatio(contentMode: .fill)
                        .frame(width: 60, height: 60)
                        .clipped()
                        .cornerRadius(10)
VStack(alignment: .leading) {
                Text("Article by")
                            .font(.custom("AvenirNext-Regular", size: 15))
                            .foregroundColor(.gray)
Text("Johne Doe")
                            .font(.custom("AvenirNext-Demibold", size: 15))
                    }
                }
                  .padding(.top, 20)
            }

Hint: We are using “Avenir-Next” as a custom font family.To learn more about using custom fonts in SwiftUI, check out this instagram post.

This is what your app preview should show so far:

Below this HStack, we can now insert the headline and some meta information, for instance the date when the post was published and its reading length.

VStack(alignment: .leading) {
                HStack {
                    //...
                }
                    .padding(.top, 20)
                Text("Lorem ipsum dolor sit amet")
                    .font(.custom("AvenirNext-Bold", size: 30))
                    .lineLimit(nil)
                    .padding(.top, 10)
                Text("3 min read • 22. November 2019")
                    .font(.custom("AvenirNext-Regular", size: 15))
                    .foregroundColor(.gray)
                    .padding(.top, 10)
            }

Now we can insert the actual content into our VStack. For this purpose, we declare a constant outside of our ContentView that holds the post’s content as a multi-lined string.

let articleContent =

"""
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.
At vero eos et accusam et justo duo dolores et ea rebum.
Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.
At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
"""

We can now insert a Text by referring to that constant.

//...
                Text("3 min read • 22. November 2019")
                    .font(.custom("AvenirNext-Regular", size: 15))
                    .foregroundColor(.gray)
                    .padding(.top, 10)
                Text(articleContent)
                    .font(.custom("AvenirNext-Regular", size: 20))
                    .lineLimit(nil)
                    .padding(.top, 30)

Let’s define a smaller width for the whole VStack:

VStack(alignment: .leading) {
                //...
            }
                .frame(width: 350)

Great, we are already done with setting up the basic layout of our blog post section. This is how your app should look like so far:

Next, we are going to implement our header image.

Implementing the Parallax Scrolling Header ↕️

On top of the VStack in our ScrollView, we wanna add our article’s header image. For making it stretchy and for implementing the parallax effect when scrolling we need to keep track of to current ScrollView’s position. For this purpose we can use a GeometryReader.

ScrollView {
            GeometryReader { geometry in
                
            }
            VStack(alignment: .leading) {
                //...
            }
                .frame(width: 350)
        }

The GeometryReader not only reads out the dimensions of its parent views (the ScrollView) but also allows us to keep track of the current ScrollView’s position. Inside the GeometryReader we insert an image view. The image should be as high and as wide as the GeometryReader.

GeometryReader { geometry in
                Image("header")
                    .resizable()
                    .aspectRatio(contentMode: .fill)
                    .frame(width: geometry.size.width, height: geometry.size.height)
                    .clipped()
            }

Thus, we have to provide the GeometryReader with a certain frame.

GeometryReader { geometry in
                Image("header")
                    //...
            }
                .frame(height: 400)

Hint: When we are just determining the height, the GeometryReader view will be as wide as possible.

In order for our header image to fill out the whole screen, we have to tell our ScrollView that it should reach to the top, meaning that it should exceed to so-called safe area. We can do this, by adding the following modifier to our ScrollView:

ScrollView {
            //...
        }
            .edgesIgnoringSafeArea(.top)

Awesome, this is what your preview should now look like

When scrolling down the header remains static, meaning that we’ve not created any parallax effect yet. Fortunately, doing this is pretty simple. We just need to tell our header image view to continuously offset its position while we are scrolling up and down. We can do this by adding the following modifier right before the .clipped modifier.

Image("header")
                    .resizable()
                    .aspectRatio(contentMode: .fill)
                    .frame(width: geometry.size.width, height: geometry.size.height)
                    .offset(y: geometry.frame(in: .global).minY/9)
                    .clipped()

We are now determining the vertical offset of our header image by checking the currently (vertical) position of our ScrollView. We are accessing the current vertical position by referring to the minY value of our GeometryReader. By doing this, we make sure our parallax view “moves” accordingly while we are scrolling. By the way: You can divide the minY value by a certain factor to control the magnitude of the parallax effect. In our example, we are dividing by the value 7.⠀

When we now run our app in live mode and scroll down, we notice that the header is also “moving” continuously. A pretty neat parallax effect!

Making our header stretchy 🦒

To make our header image stretchy, we need to find a way to expand the height of it and pushing it to the top when scrolling “over the top” of our ScrollView.

The first thing we have to do is to recognize when the upper boundary of the ScrollView is exceeded. We can this out by using the minY value of our GeometryReader again. If the upper boundary of the ScrollView is exceeded, the vertical scroll position must be positive. In the initial state, the minY value is zero and as we scroll down it gets more and more negative.

We only want to show our existing header image with the parallax effect while scrolling downwards. So only if the minY of our GeometryReader is negative or zero .

GeometryReader { geometry in
                VStack {
                    if geometry.frame(in: .global).minY <= 0 {
                        Image("header")
                        .resizable()
                        .aspectRatio(contentMode: .fill)
                        .frame(width: geometry.size.width, height: geometry.size.height)
                        .offset(y: geometry.frame(in: .global).minY/9)
                        .clipped()
                    }
                }
            }

Hint: We have to wrap the if-statement into a container view in order to work. In our example, we use a VStack, but you could also use any other container view types.

If where are scrolling “over the top”, meaning when the GeometryReader’s minY is positive, we want to show our image header as well but with some modifications, we need for making it stretchy.

if geometry.frame(in: .global).minY <= 0 {
                        //...
                    } else {
                        Image("header")
                            .resizable()
                            .aspectRatio(contentMode: .fill)
                            .frame(width: geometry.size.width, height: geometry.size.height)
                            .clipped()
                    }

The first modification we need is to make our header image the higher the further we scroll “over the top”. We can achieve this by adding the current minY value of our ScrollView’s GeometryReader to the height of our Image’s .frame modifier.

Image("header")
                            .resizable()
                            .aspectRatio(contentMode: .fill)
                            .frame(width: geometry.size.width, height: geometry.size.height + geometry.frame(in: .global).minY)
                            .clipped()

If we now run our app in the live preview and start scrolling over the upper boundary of the ScrollView, our header begins stretching. However, it also reaches into our blog posts content.


Instead we want our header to be “glued” to the top of our screen. We can do this by offsetting it depending on the ScrollView’s position like this:

Image("header")
                            .resizable()
                            .aspectRatio(contentMode: .fill)
                            .frame(width: geometry.size.width, height: geometry.size.height + geometry.frame(in: .global).minY)
                            .clipped()
                            .offset(y: -geometry.frame(in: .global).minY)

If we now run our app again, the header not only stretches while scrolling over the upper boundary but also stays on the very top. In addition, when the maximum size of the header’s image is reached, it begins zooming in. This is due to the .fill option we have chosen for our Image’s .aspectRatio modifier.

Conclusion 🎊

Awesome, we just learned how to apply a scrolling parallax effect to our SwiftUI app. By working with the GeometryReader’s minY value, that we used for reading out the current vertical scroll position, we also saw how to add a stretchy header to our app.

The complete source code for the app can be found here.

I hope you enjoyed this tutorial! If you want to learn more about SwiftUI, check out our other tutorials! Also make sure you follow us on Instagram and subscribe to our newsletter to not miss any updates, tutorials and tips about SwiftUI and more!

Leave a Reply

Your email address will not be published. Required fields are marked *

small_c_popup.png

Covid-19 Forces you into quarantine?

Start Mastering swiftUI Today save your 33% discount

small_c_popup.png

Are you ready for a new era of iOS development?

Start learning swiftUI today - download our free e-book