Skip to content

Commit

Permalink
Async await support
Browse files Browse the repository at this point in the history
  • Loading branch information
gh123man committed May 31, 2022
1 parent db4ae16 commit d54b29f
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 35 deletions.
47 changes: 22 additions & 25 deletions Examples/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand All @@ -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)
}
Expand All @@ -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)
}
Expand All @@ -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)
}
Expand All @@ -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)
}
Expand Down Expand Up @@ -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)
}
}
28 changes: 23 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
}
}
```
8 changes: 5 additions & 3 deletions Sources/Refresher/Refresher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -209,7 +210,7 @@ public struct RefreshableScrollView<Content: View, RefreshView: View>: 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 {
Expand All @@ -225,8 +226,9 @@ public struct RefreshableScrollView<Content: View, RefreshView: View>: 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)
}
}
Expand Down
49 changes: 47 additions & 2 deletions Sources/Refresher/ScrollView+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import Foundation
import SwiftUI

extension ScrollView {
public func refresher<RefreshView>(style: Style = .default, config: Config = Config(), refreshView: @escaping (Binding<RefresherState>) -> RefreshView, action: @escaping RefreshAction) -> RefreshableScrollView<Content, RefreshView> {
public func refresher<RefreshView>(style: Style = .default,
config: Config = Config(),
refreshView: @escaping (Binding<RefresherState>) -> RefreshView,
action: @escaping RefreshAction) -> RefreshableScrollView<Content, RefreshView> {
RefreshableScrollView(axes: axes,
showsIndicators: showsIndicators,
refreshAction: action,
Expand All @@ -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,
Expand All @@ -24,3 +29,43 @@ extension ScrollView {
content: content)
}
}


extension ScrollView {
public func refresher<RefreshView>(style: Style = .default,
config: Config = Config(),
refreshView: @escaping (Binding<RefresherState>) -> RefreshView,
action: @escaping AsyncRefreshAction) -> RefreshableScrollView<Content, RefreshView> {
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)
}
}

0 comments on commit d54b29f

Please sign in to comment.