Stretchy header in SwiftUI


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.


D2c5aa08 a4fd 4cec afe1 0edf6189f08c rectangle 400 x
Alexey
Belousov

Head of Mobile Development at JetRockets