Gualtiero Frigerio
Posted on August 2, 2021
A nice addition to SwiftUI at WWDC21 is the new refreshable modifier to refresh view contents. This new feature is powered by maybe the biggest announcement at WWDC21: the new async/await pattern introduced in Swift 5.5.
While this isn’t an article about this specific topic is important to know that what we’re going to see here is powered by it, thus is not backwards compatible with older versions of SwiftUI.
One of the features iOS users love it the ability to pull down a list of content to refresh it. Up to this year, there wasn’t a first party API to implement this functionality, and if you’re interested in a solution compatible with the first version of SwiftUI you can check out my article about pull down to refresh.
We finally have that official API, is a modifier called refreshable and I’m going to show you how to use it on a List, and how to apply it to a custom view. As usual the code is available on GitHub
If you want to add the refreshable scroll view to your project via SwiftPM you can use this repository.
Refresh a List
Applying the new modifier to a List is straightforward
List(posts) { post in
PostView(post: post)
}
.refreshable {
await refreshListAsync()
}
you add the modifier .refreshable and you provide a function to refresh the content inside the closure.
As I told you, this feature is powered by async/await, so we need the new await keyword before the function call.
Let’s have a look at the declaration of refreshable
func refreshable(action: @escaping () async -> Void) -> some View
the action we are providing (our closure in the example above) is marked as async, that’s the reason why we need await. I haven’t written an article about async/await yet, but you’ll find plenty of them if you want to understand exactly what is going on.
For now, this is all you need to do to implement pull down to refresh with the new modifier. That’s because List implements the whole functionality and is able to display an indicator and hide it once the reload function (the one you implemented in the closure) ends.
Refresh a custom view
All right, refreshing a List is really easy but you may wonder how to use the new modifier in your view. Maybe you cannot use a List and you have an array of elements you display in a ForEach, the good news is that with a few more lines of code you can implement the same feature.
I’m going to show you a simple example from the repository I linked before, starting with my custom ScrollView implementing pull down to refresh. See the code here
struct ScrollViewPullRefresh<Content:View>: View {
@Environment(\.refresh) var refreshAction: RefreshAction?
init(@ViewBuilder content: @escaping () -> Content) {
self.content = content
}
var body: some View {
VStack {
if isRefreshing {
ProgressView()
}
}
GeometryReader { geometry in
ScrollView {
content()
.anchorPreference(key: OffsetPreferenceKey.self, value: .top) {
geometry[$0].y
}
}
.onPreferenceChange(OffsetPreferenceKey.self) { offset in
if offset > threshold && isRefreshing == false {
if let action = refreshAction {
Task {
isRefreshing = true
await action()
withAnimation {
isRefreshing = false
}
}
}
}
}
}
}
private var content: () -> Content
@State private var isRefreshing = false
private let threshold = 50.0
}
this is the entire implementation. I won’t explain how to actually implement the pull down to refresh part, the onPreferenceChange modifier I applied to a ScrollView. Please refer to my previous article to find out more. In a nutshell, when the scroll view offset goes over a defined threshold I can perform an action, in this case the async action configured by the caller.
But how do I now what action to execute?
Alongside the refreshable modifier SwiftUI introduced a new environment value, called refresh
@Environment(\.refresh) var refreshAction: RefreshAction?
by referring to this value, we can call the async function passed to refreshable.
What is RefreshAction? Let’s take a look at its definition
public struct RefreshAction {
...
public func callAsFunction() async
}
if you’re not familiar with callAsFunction, it is a way to treat objects as functions. In this case, you can call refreshAction() and execute the code passed to the closure of refreshable. It is not importa to know that, but I pasted the definition in case you were curious 🙂
Ok let’s get back to our example. We have refreshAction, so we know what to call when the user pulls down the ScrollView.
.onPreferenceChange(OffsetPreferenceKey.self) { offset in
if offset > threshold && isRefreshing == false {
if let action = refreshAction {
Task {
isRefreshing = true
await action()
withAnimation {
isRefreshing = false
}
}
}
}
}
his is the modifier. It is important to keep a @State variable to know whether we’re executing the refresh operation, otherwise we’d continue to call the async function as the user pulls down.
If we’re not refreshing and the action is set, we can call the async function.
What is Task? I won’t go into details, but tt is a way to execute asynchronous code inside a synchronous context. In this case we set the isRefreshing value to true, then we execute the async function (so we need the await keyword) and then set the refreshing value to false. I use withAnimation so the ProgressView disappears with an animation, otherwise you’d see the ScrollView jump at the top.
Let’s see how to refresh our custom ScrollView, full implementation here.
var body: some View {
ScrollViewPullRefresh {
VStack {
ForEach(posts) { post in
PostView(post: post)
}
}
}
.task {
posts = await getPosts()
}
.refreshable {
posts = await shufflePosts()
}
}
As you see I added the refreshable modifier and inside I assign the posts variable the result of an async function to simply shuffle the posts. Let’s take a look at it
private func shufflePosts() async -> [Post] {
await Task.sleep(2_000_000_000)
return viewModel.allPosts.shuffled()
}
In order to simulate a network call, I use Task.sleep. As the name suggest, the function does nothing and sleeps for the given amount of microseconds. I find it useful to experiment with async await without setting up an API to retrieve data, you can simply have a JSON inside the project and await for the result.
Before ending, you may have noticed the .task modifier I applied to the custom view before refreshable.
This is another addition to SwiftUI, and gives us the ability to perform an asynchronous task when the view appears. In this example I load the list of posts when the view appears, and refresh them every time the user pulls down to refresh.
Of course you can implement alternative ways to refresh a list of contents. For example you can use a Button, like this
struct RefreshableView: View {
@ObservedObject var viewModel: ListViewModel
@Environment(\.refresh) var refreshAction: RefreshAction?
var body: some View {
VStack {
Button {
if let action = refreshAction {
Task {
await action()
}
}
} label: {
Image(systemName: "arrow.counterclockwise")
}
ScrollView {
ForEach(viewModel.beers) { beer in
HStack {
CustomImageView(url: URL(string: beer.imageUrl),
placeHolder: Image(systemName: "xmark.octagon"))
.frame(width: 100, height: 100)
Text(beer.name)
Spacer()
}
}
}
}
}
}
and call the async function inside the Button’s action, wrapping it with Task.
That’s it, I really like the new API and hope you’ll find it easy to adopt in your projects.
Happy coding 🙂
Posted on August 2, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 19, 2024