A customizable, native SwiftUI refresh control for iOS 14+
- the native SwiftUI refresh control only works on iOS 15+
- the native UIKit refresh control works with ugly wrappers, but has buggy behavior with navigation views
- I needed a refresh control that could accomodate an overlay (such as appearing on top of a static image)
- This one is very customizable
If you want to see it in a real app, check out dateit
Also works well with ScrollViewLoader
First add the package to your project.
import Refresher structDetailsView:View{@Statevarrefreshed=0varbody:someView{ScrollView{Text("Details!")Text("Refreshed: \(refreshed)")}.refresher{ // Called when pulled to refresh awaitTask.sleep(seconds:2) refreshed +=1}}}async/awaitcompatible - even on iOS 14- completion callback also supported for
DispatchQueueoperations .defaultand.systemstyles (see below for details)- customizable refresh spinner (see below for example)
See: Examples for a full sample project with multiple implementations
Refresher plays nice with both Navigation views and navigation subviews.
Refresher supports an overlay mode to show a refresh indicator over fixed position content
.refresher(overlay: true)
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 inRefresher 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:
publicstructEmojiRefreshView:View{@Bindingvarstate:RefresherState@Stateprivatevarangle:Double=0.0@StateprivatevarisAnimating=falsevarforeverAnimation:Animation{Animation.linear(duration:1.0).repeatForever(autoreverses:false)}publicvarbody:someView{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 inIf you prefer to call a completion to stop the refresher:
.refresher(style:.system){ done inDispatchQueue.main.asyncAfter(deadline:.now()+.seconds(1)){ refreshed +=1done() // Call done to stop the refresher }}



