Asynchronous tasks in iOS (Swift)

Kate Dmitrieva
4 min readJan 7, 2022

In every programmer’s life there comes a time when the tenth line of code is suddenly executed way after the twentieth line and you have no idea why. Well, you’ve just discovered the magical world of asynchronous tasks and from now on your life will never be the same. Time works differently in a computer program and the cool thing is that we can create a set of corresponding rules to deal with that like responsible adults we are.

There are two main approaches to handling asynchronous tasks in the modern iOS development. One way to understand the difference between these approaches is to take a look at a simple and very common networking task — say we need to fetch some JSON data from some endpoint.

Before iOS 15: a Result-based completion handler

The getJsonDataFromURL function is simple enough, but — and this is a big but — we can’t immediately return data because there is no immediate response. It may take a second or two (or longer if your user has slow internet connection).

func getJsonDataFromURL<T: Decodable>(// optional, but useful parameter set to the most common value
dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .deferredToDate,
// same here: optional, but useful parameter set to the most common value
keyDecodingStrategy: JSONDecoder.KeyDecodingStrategy = .useDefaultKeys,
// this is the completion handler which may escape the scope of the function, hence the @escaping wrappercompletion: @escaping (Result<T, APIError>) -> Void) {//Result type is a frozen structure with only two possible values, .success or .failure (error)// making sure our app will not crashguardlet url = URL(string: urlString)else {// see the error enum in the following code snippetcompletion(.failure(.invalidURL))return}URLSession.shared.dataTask(with: url) { data, response, error in// check all three optional items that we get backguardlet myHttpResponse = response as? HTTPURLResponse,myHttpResponse.statusCode == 200else {completion(.failure(.invalidResponseStatus))return}guarderror == nilelse {completion(.failure(.dataTaskError(error!.localizedDescription)))
// we can safely unwrap it because error is not nil
return}guardlet data = dataelse {completion(.failure(.corruptData))return}let decoder = JSONDecoder()decoder.dateDecodingStrategy = dateDecodingStrategydecoder.keyDecodingStrategy = keyDecodingStrategydo {let decodedData = try decoder.decode(T.self, from: data)completion(.success(decodedData))}catch {completion(.failure(.decodingError(error.localizedDescription)))}}.resume()

And the APIError enum that we need for a neat error handling:

// LocalizedError protocol is needed to make our own custom error descriptionenum APIError: Error, LocalizedError {case invalidURLcase invalidResponseStatuscase dataTaskError(String)case corruptDatacase decodingError(String) // add enum associated value to provide error string description// enum's computed property to set custom error descriptionvar errorDescription: String? {switch self {case .invalidURL:return NSLocalizedString("The provided URL is invalid", comment: "")case .invalidResponseStatus:return NSLocalizedString("Response is not 200", comment: "")case .dataTaskError(let string):return stringcase .corruptData:return NSLocalizedString("The data is malformed", comment: "")case .decodingError(let string):return string}}

After iOS 15: Async and Await + MainActor + Task

Let’s take the same getJsonDataFromURL function and this time make it return data (notice the -> T in the end). Fun fact, in Swift we actually can use the same name to create several functions and they will be considered different — totally legit as long as they have different parameters. This is called function overloading. But back to our sheep, and if you come from JavaScript like I do, these sheep will seem really familiar:

func getJSONs<T: Decodable>(dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .deferredToDate,keyDecodingStrategy: JSONDecoder.KeyDecodingStrategy = .useDefaultKeys) async throws -> T{guardlet url = URL(string: urlString)else {throw APIError.invalidURL}do {let (data, response) = try await URLSession.shared.data(from: url)guardlet myHttpResponse = response as? HTTPURLResponse,myHttpResponse.statusCode == 200else {throw APIError.invalidResponseStatus}let decoder = JSONDecoder()decoder.dateDecodingStrategy = dateDecodingStrategydecoder.keyDecodingStrategy = keyDecodingStrategydo {let decodedData = try decoder.decode(T.self, from: data)return decodedData}catch {throw APIError.decodingError(error.localizedDescription)}}catch {throw APIError.dataTaskError(error.localizedDescription)}}

Now in the ViewModel you’ll need to decorate your function with @MainActor. This decorator dispatches the whole function to main thread. Since we are affecting the UI by changing the @Published variables values and we can't do that in the background thread.

One more thing to change in our view — we must add Task block and await to .onAppear modifier (if we are calling it there), since the whole function has gone async now and needs some special treatment:

.onAppear {Task {await model.getAsyncData()}}

or to make it even simpler :

.task {await model.getAsyncData()}

--

--