diff --git a/Examples/ContentView.swift b/Examples/ContentView.swift index 9e990b8..3b108d9 100644 --- a/Examples/ContentView.swift +++ b/Examples/ContentView.swift @@ -48,11 +48,9 @@ struct ContentView: View { Text("asdf") } } - .refresher { done in - DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { - refreshed += 1 - done() - } + .refresher { + await Task.sleep(seconds: 2) + refreshed += 1 } .navigationTitle("Refresher") } @@ -74,11 +72,9 @@ struct DetailsSearch: View { Text("Refreshed: \(refreshed)") } } - .refresher(style: .system) { done in - DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) { - refreshed += 1 - done() - } + .refresher(style: .system) { + await Task.sleep(seconds: 2) + refreshed += 1 } .navigationBarTitle("", displayMode: .inline) } @@ -100,11 +96,9 @@ struct DetailsView: View { Text("Refreshed: \(refreshed)") } } - .refresher(style: style) { done in - DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) { - refreshed += 1 - done() - } + .refresher(style: style) { + await Task.sleep(seconds: 2) + refreshed += 1 } .navigationBarTitle("", displayMode: .inline) } @@ -128,11 +122,9 @@ struct DetailsOverlayView: View { Text("Refreshed: \(refreshed)") } } - .refresher(style: .overlay) { done in - DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) { - refreshed += 1 - done() - } + .refresher(style: .overlay) { + await Task.sleep(seconds: 2) + refreshed += 1 } .navigationBarTitle("", displayMode: .inline) } @@ -150,11 +142,9 @@ struct DetailsCustom: View { Text("Refreshed: \(refreshed)") } } - .refresher(refreshView: EmojiRefreshView.init ) { done in - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1500)) { - refreshed += 1 - done() - } + .refresher(refreshView: EmojiRefreshView.init ) { + await Task.sleep(seconds: 1.5) + refreshed += 1 } .navigationBarTitle("", displayMode: .inline) } @@ -202,3 +192,10 @@ struct ContentView_Previews: PreviewProvider { DetailsOverlayView() } } + +extension Task where Success == Never, Failure == Never { + static func sleep(seconds: Double) async { + let duration = UInt64(seconds * 1_000_000_000) + try! await Task.sleep(nanoseconds: duration) + } +} diff --git a/README.md b/README.md index 0ff66b6..eb3731e 100644 --- a/README.md +++ b/README.md @@ -27,16 +27,22 @@ struct DetailsView: View { Text("Refreshed: \(refreshed)") } .refresher { done in // Called when pulled to refresh - DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { - refreshed += 1 - done() // Stops the refresh view (can be called on a background thread) - } + await Task.sleep(seconds: 2) + refreshed += 1 } } } - ``` +## Features + - `async`/`await` compatible - even on iOS 14 + - completion callback also supported for `DispatchQueue` operations + - `.default` and `.system` styles (see below for details) + - customizable refresh spinner (see below for example) + + +## Examples and usage + See: [Examples](/Examples/) for a full sample project with multiple implementations ### Navigation view @@ -111,3 +117,15 @@ Add the custom refresherView: ``` ![Custom](/images/4.gif) + +## Completion handler + +If you prefer to call a completion to stop the refresher: +```swift +.refresher(style: .system) { done in + DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { + refreshed += 1 + done() + } +} +``` \ No newline at end of file diff --git a/Sources/Refresher/Refresher.swift b/Sources/Refresher/Refresher.swift index 2dc33d4..45ded24 100644 --- a/Sources/Refresher/Refresher.swift +++ b/Sources/Refresher/Refresher.swift @@ -3,6 +3,7 @@ import SwiftUI import Introspect public typealias RefreshAction = (_ completion: @escaping () -> ()) -> () +public typealias AsyncRefreshAction = () async -> () public struct Config { /// Drag distance needed to trigger a refresh @@ -209,7 +210,7 @@ public struct RefreshableScrollView: View { state.dragPosition = normalize(from: 0, to: config.refreshAt, by: distance) guard canRefresh else { - canRefresh = distance <= config.resetPoint && state.mode == .notRefreshing + canRefresh = distance <= config.resetPoint && state.mode == .notRefreshing && !isFingerDown return } guard distance > 0, showRefreshControls else { @@ -225,8 +226,9 @@ public struct RefreshableScrollView: View { set(mode: .refreshing) canRefresh = false - refreshAction { - DispatchQueue.main.asyncAfter(deadline: .now() + config.holdTime) { + + DispatchQueue.main.asyncAfter(deadline: .now() + config.holdTime) { + refreshAction { set(mode: .notRefreshing) } } diff --git a/Sources/Refresher/ScrollView+Extensions.swift b/Sources/Refresher/ScrollView+Extensions.swift index ad6fedc..eb561e1 100644 --- a/Sources/Refresher/ScrollView+Extensions.swift +++ b/Sources/Refresher/ScrollView+Extensions.swift @@ -2,7 +2,10 @@ import Foundation import SwiftUI extension ScrollView { - public func refresher(style: Style = .default, config: Config = Config(), refreshView: @escaping (Binding) -> RefreshView, action: @escaping RefreshAction) -> RefreshableScrollView { + public func refresher(style: Style = .default, + config: Config = Config(), + refreshView: @escaping (Binding) -> RefreshView, + action: @escaping RefreshAction) -> RefreshableScrollView { RefreshableScrollView(axes: axes, showsIndicators: showsIndicators, refreshAction: action, @@ -14,7 +17,9 @@ extension ScrollView { } extension ScrollView { - public func refresher(style: Style = .default, config: Config = Config(), action: @escaping RefreshAction) -> some View { + public func refresher(style: Style = .default, + config: Config = Config(), + action: @escaping RefreshAction) -> some View { RefreshableScrollView(axes: axes, showsIndicators: showsIndicators, refreshAction: action, @@ -24,3 +29,43 @@ extension ScrollView { content: content) } } + + +extension ScrollView { + public func refresher(style: Style = .default, + config: Config = Config(), + refreshView: @escaping (Binding) -> RefreshView, + action: @escaping AsyncRefreshAction) -> RefreshableScrollView { + RefreshableScrollView(axes: axes, + showsIndicators: showsIndicators, + refreshAction: { done in + Task { @MainActor in + await action() + done() + } + }, + style: style, + config: config, + refreshView: refreshView, + content: content) + } +} + +extension ScrollView { + public func refresher(style: Style = .default, + config: Config = Config(), + action: @escaping AsyncRefreshAction) -> some View { + RefreshableScrollView(axes: axes, + showsIndicators: showsIndicators, + refreshAction: { done in + Task { @MainActor in + await action() + done() + } + }, + style: style, + config: config, + refreshView: DefaultRefreshView.init, + content: content) + } +}