siteline / swiftui-introspect

Introspect underlying UIKit/AppKit components from SwiftUI
MIT License
5.59k stars 346 forks source link

Introspect prevents ScrollView `.scrollPosition` to work properly #379

Closed jorgej-ramos closed 11 months ago

jorgej-ramos commented 11 months ago

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
davdroman commented 11 months ago

You can't just override the entire delegate. See https://github.com/siteline/swiftui-introspect/issues/363#issuecomment-1722529611 for details.

jorgej-ramos commented 11 months ago

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!