SwiftUI Scroll Magic

Scroll views in SwiftUI are very straight-forward but give us no information or control over the current position of its content. So how can we detect for instance that the user has scrolled beyond certain threshold off the top?

Suppose we have some scrollable content with a title bar that is outside the ScrollView.

public struct JumpingTitleBar: View {
  
  var scrollView: some View {
    ScrollView(.vertical) {
      Text("""
      Scroll down to see the title bar jump from top to bottom.
      """)
    }
  }

  var topBar: some View {  }  
  var bottomBar: some View {  }

  var body: some View {
    ZStack {
      scrollView
      VStack {
        topBar
        Spacer()
        bottomBar
      }
    }
  }
}

We want the title bar to jump to the bottom once the user started scrolling down the text. Let’s start by adding an onTop state variable to represent this behavior.

@State var onTop = true

var body: some View {
  ZStack {
    scrollView
    VStack {
      if onTop { topBar }
      Spacer()
      if !onTop { bottomBar }
    }
  }
}

Now for the magic bit of the article, how do we actually find out what the offset of the contents is at all times? For this we leverage .anchorPreference.

Anchor preferences let us compile geometry data about our descendants, of which our scrollable Text is part. We start by defining the key type with a default value and a reducer.

struct OffsetKey: PreferenceKey {
  static var defaultValue: CGFloat = 0
  static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
    value = nextValue()
  }
}

From the text view itself we report our top anchor’s y position to our ancestors.

  GeometryReader { g in
    ScrollView(.vertical) {
      Text()
      .anchorPreference(key: OffsetKey.self, value: .top) {
        g[$0].y
      }
      

We will catch the offset value on the flip side using the onPreferenceChange() modifier on the scroll view itself.

var body: some View {
  ZStack {
    scrollView
    .onPreferenceChange(OffsetKey.self) {
    
    }
    

At this point the only thing remaining is to update our internal state according to the received offset.


scrollView
.onPreferenceChange(OffsetKey.self) {
  if $0 < -10 {
    self.onTop = false
  } else {
    self.onTop = true
  }
}

The title bar now slides away shortly after we start scrolling down and the footer instantly fades in. The whole action reverses when scrolling all the way back to the top.

For sample code featuring this and other techniques please checkout our working examples repo.

FEATURED EXAMPLE
Scroll Magic
See the title bar jump from top to bottom