SwiftUI/UIKit Introspection broken on iOS 14

Viewed 1813

I have an app that uses a UIViewRepresentable and view introspection to create pull to refresh functionality for ScrollViews in my SwiftUI app and it worked in iOS 13. However, iOS 14 has broken it and I don't know how to fix it. Here is the implementation:

import SwiftUI
import Introspect

private struct PullToRefresh: UIViewRepresentable {
    
    @Binding var isShowing: Bool
    let onRefresh: () -> Void
    
    public init(
        isShowing: Binding<Bool>,
        onRefresh: @escaping () -> Void
    ) {
        _isShowing = isShowing
        self.onRefresh = onRefresh
    }
    
    public class Coordinator {
        let onRefresh: () -> Void
        let isShowing: Binding<Bool>
        
        init(
            onRefresh: @escaping () -> Void,
            isShowing: Binding<Bool>
        ) {
            self.onRefresh = onRefresh
            self.isShowing = isShowing
        }
        
        @objc
        func onValueChanged() {
            isShowing.wrappedValue = true
            onRefresh()
        }
    }
    
    public func makeUIView(context: UIViewRepresentableContext<PullToRefresh>) -> UIView {
        let view = UIView(frame: .zero)
        view.isHidden = true
        view.isUserInteractionEnabled = false
        return view
    }
    
    private func tableView(entry: UIView) -> UIScrollView? {
        
        // Search in ancestors - FAILS HERE
        if let tableView = Introspect.findAncestor(ofType: UIScrollView.self, from: entry) {
            return tableView
        }

        guard let viewHost = Introspect.findViewHost(from: entry) else {
            return nil
        }

        // Search in siblings - FAILS HERE
        return Introspect.previousSibling(containing: UIScrollView.self, from: viewHost)
    }

    public func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<PullToRefresh>) {
        
        DispatchQueue.main.asyncAfter(deadline: .now()) {
            
            guard let tableView = self.tableView(entry: uiView) else {
                return
            }
            
            if let refreshControl = tableView.refreshControl {
                if self.isShowing {
                    refreshControl.beginRefreshing()
                } else {
                    refreshControl.endRefreshing()
                }
                return
            }
            
            let refreshControl = UIRefreshControl()
            refreshControl.addTarget(context.coordinator, action: #selector(Coordinator.onValueChanged), for: .valueChanged)
            tableView.refreshControl = refreshControl
        }
    }
    
    public func makeCoordinator() -> Coordinator {
        return Coordinator(onRefresh: onRefresh, isShowing: $isShowing)
    }
}

extension View {
    public func pullToRefresh(isShowing: Binding<Bool>, onRefresh: @escaping () -> Void) -> some View {
        return overlay(
            PullToRefresh(isShowing: isShowing, onRefresh: onRefresh)
                .frame(width: 0, height: 0)
            , alignment: .center)
    }
}

This code is slightly modified from this repository.

Here is a rough version of the usage:

NavigationView {
            VStack(alignment: .center) {
                ...
                GeometryReader { geometry in
                    ScrollView(.vertical) {
                        ...
                    }
                    .pullToRefresh(isShowing: self.$refreshingEvents, onRefresh: refreshEvents)
                }
            }.navigationBarTitle("Events", displayMode: .inline)
        }

When I use this, the ScrollView doesn't have a refresh indicator attached. Moving the pullToRefresh to a child view of the ScrollView doesn't change anything. Here is what is returned from introspecting:

PlatformViewHost<PlatformViewRepresentableAdaptor<PullToRefresh>>
_UIHostingView<ModifiedContent<Element, StyleContextWriter<SidebarStyleContext>>>
UIViewControllerWrapperView
UINavigationTransitionView
UILayoutContainerView
_UIPanelControllerContentView
_UISplitViewControllerPanelImplView
PlatformViewHost<PlatformViewControllerRepresentableAdaptor<MulticolumnSplitViewRepresentable<Element, Never, _UnaryViewAdaptor<EmptyView>>>>
_UIHostingView<_ViewList_View>
UIViewControllerWrapperView
UITransitionView
UILayoutContainerView
PlatformViewHost<PlatformViewControllerRepresentableAdaptor<UIKitTabView>>
_UIHostingView<ModifiedContent<MainView, _EnvironmentKeyWritingModifier<Optional<UserData>>>>
UIDropShadowView
UITransitionView
UIWindow

There should be a UIScrollView somewhere in there, but there isn't on iOS 14. Is there a way I could fix my current code or update it with an alternative that would retain the same functionality?

0 Answers
Related