“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, in
guard let
, in thereturn
Easy 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 one
Task
First, downloadimage
And then download itimageMetadata
After 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-await
Will not beBlocking main thread
At the same timeTask
Encountered,await
Later missions will behang
And wait forawait
When the mission is done, it will return to thehang
Continue 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 groupTask
And add image to the array. The compiler won’t let us do that, so it throwsMutation of capture var xxxx in concurrenly-excuting code
Why? Multiple tasks are referenced simultaneouslyDetailImages variable
, if there are two simultaneous tasks todetailImages
It writes data into itData Races
It’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 group
An 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 of
async await
,Asynchronous serial port
The execution. - 2, the use of
async let
At the same timeThe Task (Task)
Inside,Asynchronous parallel
The execution. - 3, the use of
group task
andfor await
, let the moreTask
Parallel 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! .