“This is the first day of my participation in the Gwen Challenge in November. Check out the details: The last Gwen Challenge in 2021”

Those of you who have used ES6 or Dart development should be familiar with async await. In iOS, with Xcode 13 and Swift 5.5 updates, it is now possible to use async await to program asynchronously in Swift. In this article, I combine my own practical experience in the work, to sum up some of their own development experience.

Problems with using callbacks

In iOS development, when performing asynchronous operations, we usually return the result of asynchronous processing via a Complete Handler. Let’s look at the following code:

func processImageData2a(completionBlock: (_ result: Image? , _ error: Error?) -> Void) { loadWebResource("dataprofile.txt") { dataResource, error in guard let dataResource = dataResource else { completionBlock(nil, error) return } loadWebResource("imagedata.dat") { imageResource, error in // 2 guard let imageResource = imageResource else { completionBlock(nil, error) return } decodeImage(dataResource, imageResource) { imageTmp, error in guard let imageTmp = imageTmp else { completionBlock(nil, error) return } dewarpAndCleanupImage(imageTmp) { imageResult, error in guard let imageResult = imageResult else { completionBlock(nil, error) return } completionBlock(imageResult) } } } } }Copy the code

This looks bad:

  • 1. The nested methods are too deep, which makes them poorly readable and error-prone.
  • 2, inguard let, in thereturnEasy to forget beforeHandler callback.
  • 3. Because of the large amount of code, it is not easy to intuitively see the function of this section.

Is there a better way to avoid these problems? After Xcode 13, we will be able to use async-await mode for better asynchronous programming.

async-await

Asynchronous serial port

After modifying with async-await:

func loadWebResource(_ path: String) async throws -> Resource func decodeImage(_ r1: Resource, _ r2: Resource) async throws -> Image func dewarpAndCleanupImage(_ i : Image) async throws -> Image ! [asyn-let.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c4dd77092c8f498a9993a38e81cfcddd~tplv-k3u1fbpfcp-waterm ark.image?) func processImageData() async throws -> Image { let dataResource = try await loadWebResource("dataprofile.txt") let imageResource = try await loadWebResource("imagedata.dat") let imageTmp = try await decodeImage(dataResource, imageResource) let imageResult = try await dewarpAndCleanupImage(imageTmp) return imageResult }Copy the code

The amount of code is significantly reduced, the logic is clearer, and the code is more readable. In order to better describe the async-await workflow, let’s take a look at the following example:

override func viewDidLoad() { super.viewDidLoad() Task { let image = try await downloadImage(imageNumber: 1) let metadata = try await downloadMetadata(for: 1) let detailImage = DetailedImage(image: image, metadata: Metadata) self.showimage (detailImage)} setupUI() doOtherThing()} func setupUI(){print(" initialize UI start ") sleep(1) Print (" initialize UI done ")} func doOtherThing(){print(" other things start ") print(" other things end ")} @mainactor func showImage(_ detailImage: DetailedImage) {print (" refresh the UI ") self. ImageButton. SetImage (detailImage. Image, for: .normal) } func downloadImage(imageNumber: Int) async throws -> UIImage { try Task.checkCancellation() // if Task.isCancelled { // throw ImageDownloadError.invalidMetadata // } print("downloadImage----- begin \(Thread.current)") let imageUrl = URL(string: "http://r1on82fmy.hn-bkt.clouddn.com/await\(imageNumber).jpeg")! let imageRequest = URLRequest(url: imageUrl) let (data, imageResponse) = try await URLSession.shared.data(for: imageRequest) print("downloadImage----- end ") guard let image = UIImage(data: data), (imageResponse as? HTTPURLResponse)? .statusCode == 200 else { throw ImageDownloadError.badImage } return image } func downloadMetadata(for id: Int) async throws -> ImageMetadata { try Task.checkCancellation() // if Task.isCancelled { // throw ImageDownloadError.invalidMetadata // }\ print("downloadMetadata --- begin \(Thread.current)") let metadataUrl = URL(string: "http://r1ongpxur.hn-bkt.clouddn.com/imagemeta\(id).json")! let metadataRequest = URLRequest(url: metadataUrl) let (data, metadataResponse) = try await URLSession.shared.data(for: metadataRequest) print("downloadMetadata --- end \(Thread.current)") guard (metadataResponse as? HTTPURLResponse)? .statusCode == 200 else { throw ImageDownloadError.invalidMetadata } return try JSONDecoder().decode(ImageMetadata.self,  from: data) } struct ImageMetadata: Codable { let name: String let firstAppearance: String let year: Int } struct DetailedImage { let image: UIImage let metadata: ImageMetadata } enum ImageDownloadError: Error { case badImage case invalidMetadata }Copy the code

As you can see in viewDidLoad:

  • 1, turn on oneTaskFirst, downloadimageAnd then download itimageMetadataAfter downloading, go back toThe main thread refreshes the UI.
  • 2. Initialize the UI and do some other things in the main thread.

There are two new concepts :Task and MainActor, and the reason for using Task is that we need a bridge between synchronous threads and asynchronous threads, and we need to tell the system to create an asynchronous environment, Concurrency concurrency error ‘async’ call in a function that does not support concurrency. Task starts a Task. @mainActor tells the showImage method to execute on the main thread.

Going back to the sample code itself, the code executes in this order

useasync-awaitWill not beBlocking main threadAt the same timeTaskEncountered,awaitLater missions will behangAnd wait forawaitWhen the mission is done, it will return to thehangContinue to execute. And that’s doneAsynchronous serial port.

Async-let

Looking back at the example above, metadata for downloading an image and downloading an image can be executed in parallel. We can use async-let to implement this, we add the following method

func downloadImageAndMetadata(imageNumber: Int) async throws -> DetailedImage {
        print(">>>>>>>>>> 1 \(Thread.current)")
        async let image =  downloadImage(imageNumber: imageNumber)
        async let metadata =  downloadMetadata(for: imageNumber)
        print(">>>>>>>> 2 \(Thread.current)")
        let detailImage = DetailedImage(image: try await image, metadata: try await metadata)
        print(">>>>>>>> 3 \(Thread.current)")
        return detailImage
}
Copy the code

Execute this method in ViewDidLoad

Task { let detailImage = try await downloadImageAndMetadata(imageNumber: 1) self.showimage (detailImage)} setupUI() doOtherThing() >>>>>>>>>> 1 <NSThread: 0x6000005db840>{number = 6, name = (null)} >>>>>>>> 2 <NSThread: 0x6000005db840>{number = 6, name = (null)} downloadImage----- begin <NSThread: 0x6000005a8240>{number = 3, name = (null)} downloadMetadata --- begin <NSThread: 0x6000005a8240>{number = 3, name = (null)} downloadImage----- end downloadMetadata --- end <NSThread: 0x6000005acf80>{number = 5, name = (null)} >>>>>>>> 3 <NSThread: 0x6000005ACF80 >{Number = 5, Name = (NULL)} Initialize UI Finish other things Start Other things End Refresh UICopy the code

The order of operation is this

With the ASyn let modifier, the function is executed concurrently, so async let is also called concurrent binding. The downloadImage is suspended and the thread continues to perform other tasks until it encounters a try await image. This is why print2 is executed before downloadImage.

Here, we execute tasks asynchronously and concurrently within a Task, and the system maintains a Task tree for us

DownloadImage and downloadMetadata are subtasks of the Task that will throw an exception if one of its subtasks throws an exception.

Group Task

Let’s think about what we would do if we wanted to download more than one image at a time. Let’s try this first:Open multiple by iterating through the number groupTaskAnd add image to the array. The compiler won’t let us do that, so it throwsMutation of capture var xxxx in concurrenly-excuting codeWhy? Multiple tasks are referenced simultaneouslyDetailImages variable, if there are two simultaneous tasks todetailImagesIt writes data into itData RacesIt’s not safe.

We can solve this problem by putting each Task into a data Task group, adding the following method

func downloadMultipleImagesWithMetadata(imageNumbers: [Int]) async throws -> [DetailedImage]{ var imagesMetadata: [DetailedImage] = [] try await withThrowingTaskGroup(of: Detailedimage. self) {group in for imageNumber in imageNumbers {// Add group.addTask(priority: .medium) { async let image = self.downloadImageAndMetadata(imageNumber: ImageNumber) return try await image}} for try await imageDetail in group { imagesMetadata.append(imageDetail) } } return imagesMetadata }Copy the code

Call this method in viewDidLoad

Task { do { let images = try await downloadMultipleImagesWithMetadata(imageNumbers: . [1, 2, 3, 4])} catch ImageDownloadError badImage {print (" images are downloaded failure ")}}Copy the code

The result is as follows

downloadImage----- begin <NSThread: 0x600001998c80>{number = 5, name = (null)}
downloadMetadata --- begin <NSThread: 0x600001998c80>{number = 5, name = (null)}
downloadImage----- begin <NSThread: 0x600001998c80>{number = 5, name = (null)}
downloadMetadata --- begin <NSThread: 0x600001998c80>{number = 5, name = (null)}
downloadImage----- begin <NSThread: 0x600001998c80>{number = 5, name = (null)}
downloadMetadata --- begin <NSThread: 0x600001998c80>{number = 5, name = (null)}
downloadImage----- begin <NSThread: 0x600001998c80>{number = 5, name = (null)}
downloadMetadata --- begin <NSThread: 0x600001998c80>{number = 5, name = (null)}
downloadImage----- end
downloadMetadata --- end  <NSThread: 0x60000198cf40>{number = 7, name = (null)}
downloadImage----- end
downloadMetadata --- end  <NSThread: 0x60000198cf40>{number = 7, name = (null)}
downloadImage----- end
downloadMetadata --- end  <NSThread: 0x60000198cf40>{number = 7, name = (null)}
downloadImage----- end
downloadMetadata --- end  <NSThread: 0x60000198cf40>{number = 7, name = (null)}
Copy the code

As you can see, multiple tasks are executed in parallel, and within a single task, they are executed in parallel.

WithThrowingTaskGroup creates a task group to hold tasks. Use “for await” to wait for all tasks in the thread to complete execution, and then return all data. In this way, the problem of data competition caused by multiple tasks in parallel is solved. The task tree structure is as follows

If one of the tasks throws an exception, that wholeTask groupAn exception will be thrown.

Asynchronous properties

A property value can be obtained asynchronously with async await, which can only be a read-only property.

extension UIImage {
    // only read-only properties can be async
    var thumbnail: UIImage? {
        get async {
            let size = CGSize(width: 40, height: 40)
            return await self.byPreparingThumbnail(ofSize: size)
        }
    }
}
Copy the code

How to access async await

Use the system async await API

The system provides us with many async apis, such as URLSession, which we can use directly.

let (data, metadataResponse) = try await URLSession.shared.data(for: metadataRequest)
Copy the code

Modify callbacks based on handlers

In some third libraries, or in some self-written methods, many are based on handler callbacks that we need to modify ourselves, such as the following callback:

//MARK: call back based
func requestUserAgeBaseCallBack(_ completeHandler: @escaping (Int)->() ){
    NetworkManager<Int>.netWorkRequest("url") { response, error in
        completeHandler(response?.data ?? 0)
    }
}
Copy the code

You can select this function, press Command + Shift + A, select Add Async Alternative, and Xcode will automatically generate Async Alternative methods for you

The conversion result is as follows:

//MARK: call back based
@available(*, deprecated, message: "Prefer async alternative instead")
func requestUserAgeBaseCallBack(_ completeHandler: @escaping (Int)->() ){
    Task {
        let result = await requestUserAgeBaseCallBack()
        completeHandler(result)
    }
}

func requestUserAgeBaseCallBack() async -> Int {
    return await withCheckedContinuation { continuation in
        NetworkManager<Int>.netWorkRequest("url") { response, error in
            continuation.resume(returning: response?.data ?? 0)
        }
    }
}
Copy the code

You can also copy this format yourself, using a CheckedContinuation.

Modify delegate-based callbacks

Through reforming system UIImagePickerControllerDelegate, we talk about this process:

class ImagePickerDelegate: NSObject, UINavigationControllerDelegate & UIImagePickerControllerDelegate { var contination: CheckedContinuation<UIImage? , Never>? @MainActor func chooseImageFromPhotoLibrary() async throws -> UIImage? {let vc = UIImagePickerController() vc.sourceType =.photolibrary vc.delegate = self print(">>>>>>>> image select \(Thread.current)") BasicTool.currentViewController()?.present(vc, animated: true, completion: nil) return await withCheckedContinuation({ continuation in self.contination = continuation }) } func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { self.contination?.resume(returning: nil) picker.dismiss(animated: true, completion: nil) } func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage self.contination?.resume(returning: image) picker.dismiss(animated: true, completion: nil) } }Copy the code

How do you use it?

Task {
    let pickerDelegate = ImagePickerDelegate()
    let image = try? await pickerDelegate.chooseImageFromPhotoLibrary()
    sender.setImage(image, for: .normal)
}
Copy the code

With the CheckedContinuation instance we can modify the delegate.

conclusion

We started by talking about the inconvenience of using callbacks, introducing swift 5.5’s new async await feature for asynchronous programming.

  • 1, the use ofasync await,Asynchronous serial portThe execution.
  • 2, the use ofasync letAt the same timeThe Task (Task)Inside,Asynchronous parallelThe execution.
  • 3, the use ofgroup taskandfor await, let the moreTaskParallel execution.

In the last section, we made async await changes to both handler – and delegate-based code.

The code mentioned in this article has been uploaded to my Git repository and can be downloaded if necessary.

If you feel that there is a harvest please follow the way to a love three even: 👍 : a praise to encourage. 🌟 : Collect articles, easy to look back! . 💬 : Comment exchange, mutual progress! .