Categories
Swift SwiftUI

ScrollView phase changes on iOS 18

In addition to scroll related view modifiers covered in the previous blog post, there is another one for detecting scroll view phases aka the state of the scrolling. The new view modifier is called onScrollPhaseChange(_:) and has three arguments in the change closure: old phase, new phase and a context.

ScrollPhase is an enum with the following values:

  • animating – animating the content offset
  • decelerating – user interaction stopped and scroll velocity is decelerating
  • idle – no scrolling
  • interacting – user is interacting
  • tracking – potential user initiated scroll event is going to happen

The enum has a convenience property of isScrolling which is true when the phase is not idle.

ScrollPhaseChangeContext captures additional information about the scroll state, and it is the third argument of the closure. The type gives access to the current ScrollGeometry and the velocity of the scroll view.

Here is an example of a scroll view which has the new view modifier attached.

struct ContentView: View {
@State private var scrollState: (
phase: ScrollPhase,
context: ScrollPhaseChangeContext
)?
let data = (0..<100).map({ "Item \($0)" })
var body: some View {
NavigationStack {
ScrollView {
ForEach(data, id: \.self) { item in
Text(item)
.frame(maxWidth: .infinity)
.padding()
.background {
RoundedRectangle(cornerRadius: 8)
.fill(Color.cyan)
}
.padding(.horizontal, 8)
}
}
.onScrollPhaseChange { oldPhase, newPhase, context in
scrollState = (newPhase, context)
}
Divider()
VStack {
Text(scrollStateDescription)
}
.font(.footnote.monospaced())
.padding()
}
}
private var scrollStateDescription: String {
guard let scrollState else { return "" }
let velocity: String = {
guard let velocity = scrollState.context.velocity else { return "none" }
return "\(velocity)"
}()
let geometry = scrollState.context.geometry
return """
State at the scroll phase change
Scrolling=\(scrollState.phase.isScrolling)
Phase=\(scrollState.phase)
Velocity
\(velocity)
Content offset
\(geometry.contentOffset)
Visible rect
\(geometry.visibleRect.integral)
"""
}
}

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
Swift SwiftUI

Scroll geometry and position view modifiers in SwiftUI on iOS 18

WWDC’24 brought some updates to scrolling. One of which is onScrollGeometryChange(for:of:action:) what we can use for reacting to scroll geometry changes. The view modifier has two closures, where the first one is transforming the scroll geometry into an arbitrary equatable type of our liking. If that value changes, the action closure is called. It is a convenient way for triggering view updates or updating other states.

The new ScrollGeometry type provides the current scroll state:

  • bounds
  • containerSize
  • contentInsets
  • contentOffset
  • contentSize
  • visibleRect
var body: some View {
List(items, id: \.self) { item in
Text(item)
}
.onScrollGeometryChange(
for: CGRect.self,
of: { scrollGeometry in
scrollGeometry.visibleRect
},
action: { oldValue, newValue in
print("visibleRect =", newValue)
}
)
}
view raw Scroll.swift hosted with ❤ by GitHub

Here is another example where we can use the new modifier for showing a scroll to top button in combination with the new scrollPosition(_:anchor:) view modifier.

struct ContentView: View {
let items: [String] = (0..<100).map({ "Item \($0)" })
@State private var canScrollToTop = false
@State private var scrollPosition = ScrollPosition(idType: String.self)
var body: some View {
ScrollView {
ForEach(items, id: \.self) { item in
Text(item)
.frame(maxWidth: .infinity)
.padding()
.onScrollVisibilityChange() { visible in
print(item, visible)
}
}
}
.scrollPosition($scrollPosition)
.overlay(alignment: .top) {
if canScrollToTop {
Button("Top") {
withAnimation {
scrollPosition.scrollTo(edge: .top)
}
}
}
}
.onScrollGeometryChange(
for: Bool.self,
of: { scrollGeometry in
scrollGeometry.contentOffset.y > 50
},
action: { oldValue, newValue in
canScrollToTop = newValue
}
)
}
}

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.