Skip to content

Instantly share code, notes, and snippets.

@bocato
Last active December 15, 2022 12:56
Show Gist options
  • Save bocato/ca1e9210aa8cc56d95104004d087d3c0 to your computer and use it in GitHub Desktop.
Save bocato/ca1e9210aa8cc56d95104004d087d3c0 to your computer and use it in GitHub Desktop.
Refresh Control for iOS 14 or less
import SwiftUI
import Introspect
import UIKit
struct ContentView: View {
@ObservedObject private var viewModel = ViewModel()
var body: some View {
NavigationView {
Group {
if viewModel.isLoading {
Text("Loading...")
} else {
ScrollView {
VStack {
ForEach(viewModel.items, id: \.self) { item in
Text(item)
.padding()
}
}
}
.onPullToRefresh {
self.viewModel.fetchItems()
}
}
}
.navigationBarTitle("My List")
.onAppear {
self.viewModel.fetchItems()
}
}
}
}
final class RefreshableContextController: ObservableObject {
private var onValueChanged: (@Sendable (_ refreshControl: UIRefreshControl) async -> Void)?
func setupRefreshControl(for scrollView: UIScrollView, onValueChanged: @escaping @Sendable (_ refreshControl: UIRefreshControl) async -> Void) {
let refreshControl = UIRefreshControl()
refreshControl.addTarget(
self,
action: #selector(self.onValueChanged(sender:)),
for: .valueChanged
)
scrollView.refreshControl = refreshControl
self.onValueChanged = onValueChanged
}
@objc private func onValueChanged(sender: UIRefreshControl) {
Task { @MainActor in
await onValueChanged?(sender)
}
}
}
struct RefreshableContext<Content: View>: View {
@StateObject var controller = RefreshableContextController()
let content: () -> Content
let onRefresh: @Sendable () async -> Void
init(
content: @escaping () -> Content,
onRefresh: @escaping @Sendable () async -> Void
) {
self.content = content
self.onRefresh = onRefresh
}
var body: some View {
content()
.introspectScrollView { scrollView in
controller.setupRefreshControl(for: scrollView) { _ in
await onRefresh()
}
}
}
}
extension View {
@ViewBuilder
func onPullToRefresh(action: @escaping () -> Void) -> some View {
self.onPullToRefreshAsync { action() }
}
@ViewBuilder
func onPullToRefreshAsync(action: @escaping @Sendable () async -> Void) -> some View {
if #available(iOS 15.0, *) {
self.refreshable(action: action)
} else {
RefreshableContext(
content: { self },
onRefresh: action
)
}
}
}
class ViewModel: ObservableObject {
@Published var items = [String]()
@Published var isLoading = false
func fetchItems() {
isLoading = true
DispatchQueue.main.asyncAfter(deadline: .now()+1) {
self.items = (0..<10).map { "Item \($0)" }
self.isLoading = false
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment