Here in Xcode 16, you can see the demo project. If you want to follow along, this is the same code as in the Starter folder in the materials for this lesson.
To get Xcode to give you warnings, move the minimum iOS Target to 18.0. Here, on the General tab of the settings for the target, you can make the change. This looks somewhat different from Xcode 15, but click here and then type in the minimum requirement of 18.0.
For this demo, you’ll update the app’s text-recognition feature because that’s the one that works pretty well on the simulator.
Open TextDetectionViewModel. You can see a warning about the deprecated .usesCPUOnly. Replace that with the temporary, hacky code from the lesson.
let supportedDevices = try! textDetectionRequest
.supportedComputeStageDevices
if let mainStage = supportedDevices[.main] {
if let cpuDevice = mainStage.first(where: { device in
device.description.contains("CPU")
}) {
textDetectionRequest.setComputeDevice(cpuDevice, for: .main)
}
}
You’ll also mark this detectText function as async, so just add the keyword async here in the function signature.
Now, remove all the request completion block code and just initialize the request with empty brackets.
let textDetectionRequest = VNRecognizeTextRequest()
Observant watchers might wonder why the code uses VNRecognizeTextRequest and not RecognizeTextRequest. In the version of iOS18 and Xcode for this demo, there seems to be a little issue with using RecognizeTextRequest in that it doesn’t believe that it’s a type of VisionRequest, so some of those properties throw errors. Once iOS 18 is out of beta, you should try to make the code work with RecognizeTextRequest.
All the completion code will go below. Now, you can see how the processing of the observations will be in the code closer to where you actually perform the work. All the code for preprocessing the image can stay, and the actual creation of the request handler remains unchanged.
The observation processing code can go down here now in the do/try/catch block, right after the .perform line, so it’s much easier to follow the logic.
First, you process the case where no text rectangles are found.
guard let results = textDetectionRequest.results else {
self.textRectangles = []
return
}
Because you’re not working with a completion block anymore, there’s no need to pass in [weak self] or to use the self? format. Also, you don’t have to downcast to [VNRecognizedTextObservation]? for the array of observations because it’s defined as the type of the results property of the request instead of being a parameter to a closure.
Now, process the observations of text:
self.textRectangles = results.compactMap { observation in
guard let topCandidate = observation.topCandidates(1).first
else { return nil }
return (observation.boundingBox, topCandidate.string)
}
If you wanted to, instead of return nil in the guard, you could throw an error and handle it down in the catch block. Notice that there isn’t an extra error parameter to check because the .perform call throws any errors.
The last step is to move over to the TextDetectionView and call the new async function correctly.
Update the code that executes with the Detect Text button to use a Task and to await.
Task {
await viewModel.detectText()
viewModel.calculateMaxTextHeight()
}
Build and run and the code works just as before. But now it’s using the modern concurrency patterns and is ready for the future.