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.