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?