In this tutorial we will be looking at how to use async/await as well as the Task struct, which is a unit of asynchronous work. If you are new to these terms, then by the end of this, you’ll have a greater understanding of what they are and how they can benefit your app.
The project we will write today will reach out to a server to fetch a JSON response that contains users. In the tutorial we will see how we can use Task to make this work. We will also use async/await and see where this is needed.
We will use an array of users found here: https://jsonplaceholder.typicode.com/users
For the purposes of this project, we will get the users name and email address. To do this, we first need a struct that will be used to decode the JSON data to:
struct User: Decodable, Identifiable {
let id: Int
let name: String
let email: String
}
We create the User struct. It conforms to Decodable because we are fetching JSON data containing users and we need the ability to convert that into a User struct. It also conforms to Identifiable because we will use an array of users in a SwiftUI List which requires that each item is identifiable.
Our properties within are an id, which is for Identifiable purposes. The other two are name and email which for this tutorial is all that we are interested in from the fetched JSON.
class UserViewModel: ObservableObject {
@Published var users: [User] = []
@Published var isLoading = false
@MainActor
func fetchUsers() async {
isLoading = true
defer { isLoading = false }
// For demo purposes to slow it down
try? await Task.sleep(nanoseconds: 4_000_000_000)
let url = URL(string: "https://jsonplaceholder.typicode.com/users")!
do {
let (data, _) = try await URLSession.shared.data(from: url)
users = try JSONDecoder().decode([User].self, from: data)
} catch {
print("Failed to fetch users: \(error.localizedDescription)")
}
}
}
In this next part of the code, we create a UserViewModel class that conforms to ObservableObject.
Lines 2 and 3 contain two @Published properties. One will contain an array of Users that we fetch from the server. The second will let subscribers know if the data is still being fetched.
On line 5 we see @MainActor which is a global actor attribute and ensures that the code runs on the main thread and is needed for code that is needed for the view. This brief description doesn’t give it justice, but its a seperate subject that will be discussed at a later date. For now, just accept its there because the result is that users will be updated, that property is @Published, and then the view subscribes to that.
On the next line we create a new method that has async after it. This tells us that fetchUsers() will run asyncronously.
Lines 7 and 8 set isLoading to true, and the defer on line 8 tells the compiler that isLoading will change to false and that this will be executed last. Even though it’s near the top of the method, defering it means it waits to the end to run, after the data has been downloaded and processed.
Line 11 has been added to introduce a delay for the purposes of showing how fetching users work. This would not be included in almost all cases. In testing, I found that the users were being fetched so quickly that the progress meter was only showing for fractions of a second, which wasn’t ideal for a tutorial trying to show something that takes longer to do. This line is a Task that we await. What it’s saying is “wait here until the 4-second timer has completed” and when done, the code carries on executing. Although the point of this tutorial is to show how we can fetch data without blocking the main thread, it might seem odd to make it sit and wait; however, this method is running asynchronously. The rest of the app is able to function while this method is about to fetch data and await information coming back. It is normal to see await in code when working with async.
Line 13 is where we make our URL. The URL struct here is initialising with a string.
Lines 15 to the end is where we fetch the data. We do this in a do-catch statement where we use try on the call to the server.
Line 16 is where we try get the data from the server. This has an await because we cannot use JSONDecoder on line 17 until we have the data to decode. If line 16 fails, then it goes to the catch and prints the error. Assuming all is OK, it then attempts to decode the data fetched. If all OK, we finish and the defer on line 8 runs which sets isLoading to false. Notice that we don’t await the JSONDecoder. This runs quickly and we don’t need to await. However, if we are working with huge amounts of data then this could need a rethink, but that is out of scope of this turorial.
We have completed this part of the tutorial where a call can be made to the function, it runs, does what it needs to do, and doesn’t hold up the view, except, we don’t have the view yet. Lets create that now.
struct ContentView: View {
@StateObject private var viewModel = UserViewModel()
var body: some View {
NavigationStack {
VStack {
if viewModel.isLoading {
ProgressView("Loading users...")
.progressViewStyle(CircularProgressViewStyle())
.padding()
} else {
List(viewModel.users) { user in
VStack(alignment: .leading) {
Text(user.name)
.font(.headline)
.foregroundColor(.primary)
Text(user.email)
.font(.subheadline)
.foregroundColor(.secondary)
}
.padding(.vertical, 8)
}
.listStyle(PlainListStyle())
.padding(.horizontal)
}
}
.navigationTitle("Users")
.onAppear {
Task {
await viewModel.fetchUsers()
}
}
}
}
}
We begin with an @StateObject which creates and owns the viewModel which is a UserViewModel(). This allows us to observe the @Published properties so that the view can update when needed.
Our view is a standard NavigationStack (line 5) with a navigationTitle set to “Users” on line 27.
We then have a VStack and in there, we show a ProgressView “if” the isLoading property is true. Remember that we set this to true or false in our view model earlier.
If its false, it means we have some users to work with which we iterate over in a List view starting on line 12. Briefly, each user in the users array is in a VStack with two Text views, one for their name and one for their email.
Other than some styling for padding and the list style, the only item left to explain is on lines 28 – 32 where we use .onAppear to call fetchUsers() on the viewModel. .onAppear is called when a view loads. Inside it we create a Task, and then await the call to fetchUsers. This lets the view know that we can carry on so that we don’t block the view while the data is fetched from the server.
An alternative way to writing this is relacing .onAppear with .task and removing Task { … } from the code.
When running the code now, you’ll see a progress meter for at least 4 seconds, at which point the data is fetched, taking fractions of a second, and then the view will update to show the list of users found in the JSON that was downloaded.
Leave a Reply
You must be logged in to post a comment.