gh123man / SwiftUI-Refresher

A native, customizable SwiftUI refresh control
MIT License
129 stars 12 forks source link
ios swift swiftui

Refresher

A customizable, native SwiftUI refresh control for iOS 14+

Why?

See it in action

If you want to see it in a real app, check out dateit

Also works well with ScrollViewLoader

Usage

First add the package to your project.

import Refresher 

struct DetailsView: View {
    @State var refreshed = 0

    var body: some View {
        ScrollView {
            Text("Details!")
            Text("Refreshed: \(refreshed)")
        }
        .refresher { // Called when pulled to refresh
            await Task.sleep(seconds: 2)
            refreshed += 1
        }
    }
}

Features

Examples and usage

See: Examples for a full sample project with multiple implementations

Navigation view

Navigation

Refresher plays nice with both Navigation views and navigation subviews.

Subview

Detail view with overlay

Refresher supports an overlay mode to show a refresh indicator over fixed position content

.refresher(overlay: true)

Overlay

System style

Refresher's default animation is designed to be more flexible than the system animation style. If you want Refresher to behave more like they system refresh control, you can change the style:

.refresher(style: .system) { done in

System

Customization

Refresher can take a custom spinner view. Your custom view will get a binding instances of the refresher state that contains useful properties for managing animations and translations. Here is a custom spinner that shows an emoji:

public struct EmojiRefreshView: View {
    @Binding var state: RefresherState
    @State private var angle: Double = 0.0
    @State private var isAnimating = false

    var foreverAnimation: Animation {
        Animation.linear(duration: 1.0)
            .repeatForever(autoreverses: false)
    }

    public var body: some View {
        VStack {
            switch state.mode {
            case .notRefreshing:
                Text("🤪")
                    .onAppear {
                        isAnimating = false
                    }
            case .pulling:
                Text("😯")
                    .rotationEffect(.degrees(360 * state.dragPosition))
            case .refreshing:
                Text("😂")
                    .rotationEffect(.degrees(self.isAnimating ? 360.0 : 0.0))
                        .onAppear {
                            withAnimation(foreverAnimation) {
                                isAnimating = true
                            }
                    }
            }
        }
        .scaleEffect(2)
    }
}

Add the custom refresherView:

.refresher(refreshView: EmojiRefreshView.init ) { done in

Custom

Completion handler

If you prefer to call a completion to stop the refresher:

.refresher(style: .system) { done in
    DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) {
        refreshed += 1
        done() // Call done to stop the refresher
    }
}