Reputation: 510
Using iOS 17 as minimum target and .scrollTargetBehavior(.paging)
modifier.
I need a callback similar to scrollViewDidEndScrollingAnimation
.
The goal is to get the callback only after the page changes and the animation finishes, not during the scrolling process or in a middle or so.
This is not a new question, but many other answer I've tried didn't worked well. Is there maybe a new solution, specially if minimum iOS target for my project is iOS17?
Upvotes: 1
Views: 360
Reputation: 273540
In iOS 18 there is onScrollPhaseChange
, so you are not concerned with forward-compatibility. You can use SwiftUI-Introspect to intercept the scroll view delegate calls.
You write your own UIScrollViewDelegate
that wraps the built-in delegate of ScrollView
(after all, SwiftUI also needs to set its own delegate for its own purposes), and forwards every delegate method.
class ScrollViewObserver: NSObject, ObservableObject, UIScrollViewDelegate {
var wrapped: (any UIScrollViewDelegate)?
var action: (() -> Void)?
weak var scrollView: UIScrollView?
func scrollViewDidScroll(_ scrollView: UIScrollView) {
wrapped?.scrollViewDidScroll?(scrollView)
}
func scrollViewDidZoom(_ scrollView: UIScrollView) {
wrapped?.scrollViewDidZoom?(scrollView)
}
func scrollViewDidScrollToTop(_ scrollView: UIScrollView) {
wrapped?.scrollViewDidScrollToTop?(scrollView)
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
wrapped?.scrollViewWillBeginDragging?(scrollView)
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
wrapped?.scrollViewDidEndDecelerating?(scrollView)
action?()
}
func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool {
wrapped?.scrollViewShouldScrollToTop?(scrollView) ?? true
}
func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) {
wrapped?.scrollViewWillBeginDecelerating?(scrollView)
}
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
wrapped?.scrollViewDidEndScrollingAnimation?(scrollView)
}
func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) {
wrapped?.scrollViewWillBeginZooming?(scrollView, with: view)
}
func scrollViewDidChangeAdjustedContentInset(_ scrollView: UIScrollView) {
wrapped?.scrollViewDidChangeAdjustedContentInset?(scrollView)
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
wrapped?.scrollViewDidEndDragging?(scrollView, willDecelerate: decelerate)
}
func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
wrapped?.scrollViewDidEndZooming?(scrollView, with: view, atScale: scale)
}
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
wrapped?.scrollViewWillEndDragging?(scrollView, withVelocity: velocity, targetContentOffset: targetContentOffset)
}
}
Then in introspect
, you can replace the scroll view's delegate with an instance of ScrollViewObserver
,
struct OnScrollEndModifier: ViewModifier {
@StateObject var observer = ScrollViewObserver()
let action: () -> Void
func body(content: Content) -> some View {
if #available(iOS 18, *) {
content
.onScrollPhaseChange { oldPhase, newPhase in
if newPhase == .idle && oldPhase != .idle {
action()
}
}
} else {
content
.introspect(.scrollView, on: .iOS(.v17)) { scrollView in
observer.action = action
// only replace the delegate when this is a new scroll view we haven't seen
if observer.scrollView != scrollView {
observer.wrapped = scrollView.delegate
scrollView.delegate = observer
observer.scrollView = scrollView
}
}
}
}
}
extension View {
func onScrollEnd(_ action: @escaping () -> Void) -> some View {
modifier(OnScrollEndModifier(action: action))
}
}
Usage:
ScrollView {
// ...
}
.scrollTargetBehavior(.paging)
.onScrollEnd {
print("Ended!")
}
Upvotes: 2