The introduction

Swift, which was born in 2014, is no spring chicken. I still remember the joy of experiencing Swift for the first time. It was much more modern, simple and elegant than the lengthy OC. But Swift’s early days were brutal and turbulent, with numerous bugs and alarms on old projects every time a new version was released, and the migration of projects was a pain for developers. I have to say that the person who dared to practice and apply in the project at the beginning of Swift is a real warrior. I only started moving projects gradually from OC to Swift in Swift 4, and only fully moved to pure Swift development in 2019 when Swift 5 achieved ABI stability.

The stability of the ABI is a sign of Swift’s maturity, but when it comes to concurrent programming, Swift is lagging behind. Chris Lattner paints an exciting picture of the future as early as 2017 in his Swift Concurrency Manifesto. The 2021 Swift 5.5 release finally adds Concurrency to the standard library, making Swift Concurrency programming easier, more efficient, and more secure.

Previously, we used closures to handle callbacks to asynchronous events. Here is an example of downloading a network image:

func fetchImage(from: String.completion: @escaping (Result<UIImage? .Error- > >)Void) {
  URLSession.shared.dataTask(with: .init(string: from)!) { data, resp, error in
    if let error = error {
      completion(.failure(error))
    } else {
      DispatchQueue.main.async {
        completion(.success(.init(data: data!)))
      }
    }
  }.resume()
}
Copy the code

The code isn’t complicated, but this is only for scenarios where a single image is downloaded. We need to design a little more complicated: first download the first two images (in no order) and show them, then download the third image and show them, when all three images are downloaded, then show them in the UI interface. Of course, the actual development is usually the first to download the image first show, here is just an example of unconventional design.

The complete implementation code becomes the following:

import UIKit

class ViewController: UIViewController {
  
  let sv = UIScrollView(frame: UIScreen.main.bounds)
  let imageViews = [UIImageView(), UIImageView(), UIImageView()]
  let from = [
    "https://images.pexels.com/photos/10646758/pexels-photo-10646758.jpeg?auto=compress&cs=tinysrgb&dpr=2&w=500"."https://images.pexels.com/photos/9391321/pexels-photo-9391321.jpeg?auto=compress&cs=tinysrgb&dpr=2&w=500"."https://images.pexels.com/photos/9801136/pexels-photo-9801136.jpeg?auto=compress&cs=tinysrgb&dpr=2&w=500"
  ]
  
  override func viewDidLoad(a) {
    super.viewDidLoad()
    
    sv.backgroundColor = .white
    view.addSubview(sv)
    sv.contentSize = .init(width: 0, height: UIScreen.main.bounds.height + 100)
    
    imageViews.enumerated().forEach { i, v in
      v.backgroundColor = .lightGray
      v.contentMode = .scaleAspectFill
      v.clipsToBounds = true
      v.frame = .init(x: 0, y: CGFloat(i) * 220, width: UIScreen.main.bounds.width, height: 200)
      sv.addSubview(v)
    }
    
    let group = DispatchGroup(a)let queue = DispatchQueue(label: "fetchImage", qos: .userInitiated, attributes: .concurrent)
    
    let itemClosure: (Int.DispatchWorkItemFlags.@escaping() - > () - >DispatchWorkItem = { idx, flags, completion in
      return DispatchWorkItem(flags: flags) {
        self.fetchImage(from: self.from[idx]) { result in
          print(idx)
          switch result {
          case let .success(image):
            self.imageViews[idx].image = image
          case let .failure(error):
            print(error)
          }
          completion()
        }
      }
    }
    
    from.enumerated().forEach { i, _ in
      group.enter()
      let flags: DispatchWorkItemFlags = (i = = 2) ? .barrier : []
      queue.async(group: group, execute: itemClosure(i, flags, {
        group.leave()
      }))
    }
    
    group.notify(queue: queue) {
      DispatchQueue.main.async {
        print("end")}}}}Copy the code

It doesn’t look too complicated to use the GCD to implement the requirements, and we can use the PromiseKit to manage the event bus instead of writing the GCD level code directly, making the code simpler and more readable. But just imagine, the actual requirements may be more complicated. We may need to obtain some data from the server first, and then download images for decoding and caching. At the same time, there may be audio and video downloads and other tasks to deal with, which will be even more complicated. With or without a library as great as PromiseKit, there are issues that will become more and more obvious as business complexity increases:

  • Closures are inherently difficult to read and have the potential to lead to circular references
  • Callbacks must cover a variety of situations, and it is difficult to identify problems if they are missed
  • Although Result handles errors well, it is difficult to solve the problem of upward transmission of errors
  • Nesting levels too deep leads to callback hell
  • .

At the beginning of the async/await experience

Concurrency’s solution to these problems is to use the async/await pattern, which is well established in C#, Javascript and other languages. Now we can finally use it in Swift!

FetchImage: async/await; async/await;

  • Async: Added to the end of a function to mark it as asynchronous
  • Await: added before an async function is called to indicate that the code there will be blocked until the asynchronous event returns
func fetchImage(idx: Int) async throws  -> UIImage { / / 1
  let request = URLRequest(url: .init(string: from[idx])!)
  / / 2
  let (data, resp) = try await URLSession.shared.data(for: request)
  / / 3
  print(idx, Thread.current)
  guard (resp as? HTTPURLResponse)?.statusCode = = 200 else {
    throw FetchImageError.badNetwork
  }
  guard let image = UIImage(data: data) else {
    throw FetchImageError.downloadFailed
  }
  return image
}
Copy the code
  1. Async throws the function asynchronously

  2. The full name of the urlsession.shared. data method is as follows, so we need to call the method with try await

    public func data(from url: URL.delegate: URLSessionTaskDelegate? = nil) async throws- > (Data.URLResponse)
    Copy the code
  3. When the code executes at this point, it indicates that the asynchronous event to download the image has ended

You are already getting a feel for using async/await: async to mark an asynchronous event, await an asynchronous event to call, wait for it to return, and then continue with the following code. Throws throw and try, which almost always occur at the same time. Throws “try await” and “async” in reverse order. Throws “try await” and “try await” in reverse order.

FetchImage (); fetchImage (); fetchImage ();

/ / 1
async let image0 = try? fetchImage(idx: 0)
async let image1 = try? fetchImage(idx: 1)
/ / 2
let images = await [image0, image1]
imageViews[0].image = images[0]
imageViews[1].image = images[1]
/ / 3
imageViews[2].image = try? await fetchImage(idx: 2)
Copy the code
  1. The async let allows multiple asynchronous events to be executed simultaneously, which means that the first two images can be downloaded asynchronously at the same time.

    Earlier we said async is used to mark asynchronous functions and await is used to call, almost always in the same place. The compiler will check to see if an async function is called with await and will report an error if no await is used. In this case, we can still compile without using await when we call fetchImage, because when we use async let, if we don’t explicitly use try await, Swift will implement it implicitly. And it can delay the call of the try await.

    It is also possible to change the above code to the following:

    async let image0 = fetchImage(idx: 0)
    async let image1 = fetchImage(idx: 1)
    let images = try await [image0, image1]
    Copy the code
  2. Await blocks the current task and waits for the two asynchronous tasks above to return results

  3. After downloading the first two images, proceed to asynchronously download and display the third image

Execute the above code in viewDidLoad and see that async is red everywhere. This is because if async is called inside a function, the function also needs to be marked async in order to provide an asynchronous environment inside the function body and pass asynchronous events. ViewDidLoad is not marked async, and the compiler notices this and reports an error. But we can’t do that. Because viewDidLoad is a method in UIViewController overridden, it’s a synchronization function that’s running in the main thread and it has to be.

So how can this problem be solved? Swift provides us with tasks, and in the closure of the Task instance we create, we will get a new asynchronous environment so that asynchronous functions can be called. Tasks are like Bridges that break down the boundaries of synchronous environments and provide us with access to asynchronous environments.

We put the above code in the closure of the Task instance, and the program runs smoothly.

Task {
  / / 1
  async let image0 = fetchImage(idx: 0)
  async let image1 = fetchImage(idx: 1)
  / / 2
  let images = try await [image0, image1]
  imageViews[0].image = images[0]
  imageViews[1].image = images[1]
  / / 3
  imageViews[2].image = try? await fetchImage(idx: 2)}Copy the code

The code above is slightly different: the first two images are downloaded asynchronously at the same time, but wait for each other until both images are downloaded before being displayed. Here we provide two ideas to achieve the same effect as before. One is to put the logic of displaying images in fetchImage method, and the other is to use Task to solve the problem. The reference code is as follows:

Task {
  let task1 = Task {
    imageViews[0].image = try? await fetchImage(idx: 0)}let task2 = Task {
    imageViews[1].image = try? await fetchImage(idx: 1)}let _ = await [task1.value, task2.value]
  imageViews[2].image = try? await fetchImage(idx: 2)}Copy the code

Tasks and Taskgroups are beyond the scope of this article and will be covered in a separate section later.

It should be added here that when we use async let, we are implicitly creating a new task, or subtask, within the current task. An async let is like an anonymous Task that is not explicitly created and cannot be stored using local variables. Therefore, task-related attributes and methods such as value and cancel() cannot be used.

An Async let is a syntactic sugar that you can use to handle asynchronous event processing in most scenarios. If the number of asynchronous events to be handled is large and the relationship is complex, even involving the priority of the event, then using Task and TaskGroup is a wise choice.

Refactor to Async

If you want to migrate previously callback-based asynchronous functions to async/await (iOS 13 minimum support), Xcode has very convenient operations built in for fast, zero-cost migration and compatibility.

As shown in the figure, select the corresponding method and right-click Refactor. There are three options:

  1. Convert Function to Async: Converts the current callback Function to Async, overwriting the current Function
  2. Add Async Alternative: Uses Async to rewrite the current callback function, and provides another callback function based on the rewritten function combined with Task
  3. Add Async Wrapper: Retain the current callback function and provide an Async function on top of it

From the above we can see that the range of iOS versions supported by Wrapper is larger than Alternative, we can operate according to the lowest supported version of the project as needed:

  • < iOS 13, choose 3
  • >= iOS 13
    • Migrate whole to Async: Select 1
    • Preserve the callback function API: choose 3 or 1

summary

Async /await simplifies the processing of asynchronous events and allows us to write safe and efficient concurrent code without having to deal directly with threads. Instead of the spaghetti code that the callback mechanism often spawned, we can use a linear structure to express concurrency intent clearly.

This is thanks to the idea behind the structured concurrency paradigm, which is similar to structured programming. Each concurrent task has its own scope and has clear and unique entrances and exits. No matter how complex the internal implementation of this concurrent task is, its exit must be a single one.

We think of concurrent tasks as a pipe, and the water flow is the task to be performed in the pipe. In the world of unstructured programming, subtasks generate many pipe branches, water flows out of different branch outlets, and failures occur. We need to process the flow at different outlets, and the more outlets we have, the more frantic we are. In the structured programming world, we don’t care about branch exits, we just guard a single exit at the other end of the pipe, no matter how complicated the branch exit, the water will eventually return to the exit of the pipe.