Two iOS 18 modifiers report what is visible in a ScrollView: onScrollTargetVisibilityChange
gives the scroll view the list of visible ids, and onScrollVisibilityChange tells one view
about its own visibility. Both take a threshold for how much of the view counts as visible.
Visible Identifiers
Mark the lazy stack with scrollTargetLayout so the children, not the stack, are the targets.
Then attach onScrollTargetVisibilityChange to the scroll view with the id type.
@State private var visible: [Int] = []
ScrollView {
LazyVStack {
ForEach(1..<100, id: \.self) { item in
Text(verbatim: item.formatted())
}
}
.scrollTargetLayout()
}
.onScrollTargetVisibilityChange(idType: Int.self) { identifiers in
visible = identifiers
}
Per-View Visibility
onScrollVisibilityChange attaches to a view inside the scroll view and reports a boolean. The
canonical use is media that plays only while on screen.
struct VideoPlayerView: View {
let url: URL
@State var player: AVPlayer?
var body: some View {
VideoPlayer(player: player)
.task {
if player == nil {
player = AVPlayer(url: url)
}
}
.onScrollVisibilityChange { isVisible in
if isVisible {
player?.play()
} else {
player?.pause()
}
}
}
}
The Threshold
Both modifiers accept threshold, the fraction of the view that must be visible before the
action fires. The default is 0.5. A threshold: 0.1 fires once 10 percent shows, and fires again
when visibility drops back under it.
.onScrollVisibilityChange(threshold: 0.1) { isVisible in
// ...
}