Stretchy header in SwiftUI

After the release of SwiftUI, many of the standard UIKit solutions have become inapplicable. On the other hand, it is now possible to give it a try and implement the familiar elements of the user interface in a declarative style. One of these elements is the stretchy header on scrolling screens.


Xcode 11 and SwiftUI are under active development, so the behavior may vary from version to version. The article is based on Xcode 11 beta 5.

Preparation

First, we will prepare the interface of the corresponding screen. As an example, let's try to implement a typical screen for the article content. You can immediately notice that there are certain problems with getting the desired result when using ScrollView.


struct ContentView: View {



    private static let formatter: DateFormatter = {

        let formatter = DateFormatter()

        formatter.dateStyle = .short

        formatter.timeStyle = .short

        return formatter

    }()



    var body: some View {

        ScrollView {

            VStack(alignment: .leading) {

                Image(kImage).resizable()

                    .frame(maxHeight: 300)



                VStack(alignment: .leading, spacing: 8) {

                    HStack {

                        Text("\(kPublishedAt, formatter: Self.formatter)")

                            .foregroundColor(.secondary)

                            .font(.caption)



                        Spacer()



                        Text("Author: \(kAuthor)")

                            .foregroundColor(.secondary)

                            .font(.caption)

                    }



                    Text(kTitle)

                        .font(.headline)



                    Text(kContent)

                        .font(.body)

                }.frame(idealHeight: .greatestFiniteMagnitude)

                    .padding()

            }

        }.edgesIgnoringSafeArea(.top)

    }   

}

Second, let's fix the problem with the fact that the image was compressed horizontally. Let’s use the GeometryReader:


struct ContentView: View {



    var body: some View {

        ScrollView {

            VStack(alignment: .leading) {

                GeometryReader { _ in

                    Image(kImage).resizable()

                        .aspectRatio(contentMode: .fill)

                }.frame(maxHeight: kHeaderHeight)



                // ...

            }

        }.edgesIgnoringSafeArea(.top)

    }



}

Implementation

In the simplest implementation, the header behaves in two ways:

  • scrolling upwards at which our element without changing the sizes hides behind the top border of the screen with other contents of ScrollView;

  • scrolling downwards with a header stretching.

Let’s rewrite these statements in the form of code:


struct ContentView: View {



    var body: some View {

        ScrollView {

            VStack(alignment: .leading) {

                GeometryReader { (geometry: GeometryProxy) in

                    if geometry.frame(in: .global).minY <= 0 {

                        Image(kImage).resizable()

                            .aspectRatio(contentMode: .fill)

                            .frame(width: geometry.size.width,

                                    height: geometry.size.height)

                    } else {

                        Image(kImage).resizable()

                            .aspectRatio(contentMode: .fill)

                            .offset(y: -geometry.frame(in: .global).minY)

                            .frame(width: geometry.size.width,

                                    height: geometry.size.height + 

                                            geometry.frame(in: .global).minY)

                    }

                }.frame(maxHeight: kHeaderHeight)



                // ...

        }.edgesIgnoringSafeArea(.top)

    }



}

To support autocompletion for an instance of GeometryProxy class inside GeometryReader we explicitly specify its type.

Result

For further use, you can design this solution as a separate component of the user interface.

The source code can be found here.


Alexey
Belousov

Head of Mobile Development at JetRockets

Explore more of JetRockets