siteline/swiftui-introspect

Introspect prevents ScrollView `.scrollPosition` to work properly

jorgej-ramos opened this issue ยท 2 comments

Description

It seems that the implementation of .introspect is somehow blocking the correct behavior of .scrollPosition in a ScrollView.

We can see it using the following example:

import SwiftUI
import SwiftUIIntrospect

struct SwiftUIView: View {
    @State private var position: Int?
    
    @StateObject private var scrollViewDelegate = ScrollViewDelegate()
    
    var body: some View {
        VStack {
            ScrollView(.horizontal) {
                LazyHStack {
                    ForEach(0..<100) { index in
                        Rectangle()
                            .fill(Color.green.gradient)
                            .frame(width: 60, height: 40)
                            .id(index)
                    }
                }
                .scrollTargetLayout()
            }
            .scrollPosition(id: $position)
            .introspect(.scrollView, on: .iOS(.v17)) { scroll in
                scroll.delegate = scrollViewDelegate
            }
            
            Text("Position: \(position ?? 0) | Scrolling: \(scrollViewDelegate.isScrolling ? "Yes" : "No")")
        }
    }
}

#Preview {
    SwiftUIView()
}

@Observable
fileprivate class ScrollViewDelegate: NSObject, ObservableObject, UIScrollViewDelegate {
    var isScrolling = false
    
    func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
        isScrolling = true
    }
    
    func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
        isScrolling = decelerate ? true : false
    }
    
    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
        isScrolling = false
    }
}

If we implement .introspect, we will stop receiving updates on position.

If we don't implement it, we won't be able to use UIScrollViewDelegate but instead .scrollPosition will work again.

I have tried placing the statement .scrollPosition(id: $position) before and/or after the implementation of .instrospect, but the result is the same in all cases.

Also, this implementation has been tested on LazyHStack before and after scrollTargetLayout() with no success.

.introspect(.scrollView, on: .iOS(.v17), scope: .ancestor) {
    $0.delegate = scrollViewDelegate
}

Checklist

Expected behavior

The expected behavior is that .scrollPosition and .introspect can coexist, in such a way that updates are received from both, and both the current position and whether the ScrollView is scrolling or not are displayed.

Actual behavior

Currently, it appears that .introspect is overriding the ability to receive updates from .scrollPosition

Steps to reproduce

  1. Copy and paste the code in description on a new project.
  2. Comment and uncomment the .introspect modifier to see the differences.

Version information

1.1.0

Destination operating system

iOS 17

Xcode version information

Version 15.0.1 (15A507)

Swift Compiler version information

swift-driver version: 1.87.1 Apple Swift version 5.9 (swiftlang-5.9.0.128.108 clang-1500.0.40.1)
Target: x86_64-apple-macosx14.0

You can't just override the entire delegate. See #363 (comment) for details.

First of all, thank you for taking the time to respond.

Indeed, taking into account the UIScrollViewDelegate that SwiftUI manages behind the scenes is mandatory so as not to break the cycles of SwiftUI's internal structure.

I had been mulling over the problem for so long that I hadn't thought about this. And it makes perfect sense. Is working.

Kudos to you for the help!