Instruction 02

New Names

Years ago, all the frameworks had two-letter prefixes. This was to help with name spacing and collisions. For example, many frameworks might have wanted something called Image. Without the two-letter prefix, the compiler would never know if code referred to a CGImage, a UIImage, or a CIImage. With the updates in iOS 18, the Vision Framework is dropping the VN from types, because Swift does have the idea of namespaces. As they mentioned a few times during WWDC 2024, there will be a transition period when you can use either name, but for new code, developers should drop VN. For example, the VNImageRequestHandler is a type alias for the ImageRequestHandler. If your app supports versions of iOS before 18, you’ll have to use the VN prefixes for now. You’ll need to decide how you want to migrate going forward.

When supporting multiple code versions, Apple gives developers #available() and @available(). You use @available() to mark a particular function or structure as only available at a particular version of iOS and the compiler helps enforce compliance. You use #available() when you want to have some branching at runtime depending on the iOS version. You’ll need to decide when to use each in your code. In the case of Vision requests, you might want code like this:

let request: VNRequest

if #available(iOS 18, *) {
  request = AnimalRecognitionRequest()
} else {
  request = VNRecognizeAnimalsRequest()
}

try? handler.perform[request]

When you’re working with the @available and #available keywords, you’ll duplicate some code. It’s just part of supporting multiple versions of iOS. It’s important to find a balance between trying to refactor everything to avoid code duplication and keeping the code easy to read. Remember, you’ll delete that pre-iOS 18 code someday anyway. This is true whether you’re using Vision or not. One thing you can rely on when working with Vision is that the requests and handlers are all classes and so they inherit. In the example above, the request is declared as the basic VNRequest. When it gets instantiated, the appropriate subclass applies.

Concurrency With Async/Await

By using the async/await pattern instead of completion block handlers, code becomes more readable. Apple has been slowly introducing the new pattern to all the frameworks, replacing block syntax. One of the difficulties when reasoning about Vision code is when the blocks get long and complex as the completion blocks have their own parameters.

Additionally, code in the completion block doesn’t automatically throw errors so in a single Swift file, error handling happens sometimes with do/try/catch and sometimes by looking for an error object. Adopting async/await makes reading the code more linear. This helps when you return to code later and don’t want to spend much time piecing things together.

let request = VNRecognizeTextRequest { request, error in
  guard let observations = request.results as?
    [VNRecognizedTextObservation],
    error == nil else {
      print("Error: \(error?.localizedDescription ?? "Unknown error")")
      return
  }

    let recognizedText = observations.compactMap
      { $0.topCandidates(1).first?.string }
    print("Recognized Text: \(recognizedText)")
}

let handler = VNImageRequestHandler(cgImage: image, options: [:])
do {
  try handler.perform([request])
} catch {
  print("Failed to perform request: \(error.localizedDescription)")
}

In the code above, you can see that some errors get handled in the do/try/catch and others get handled in the { request, error in } code block. Additionally, if there were multiple requests for the same handler, it could be easy to get lost in the forest of curly braces.

That same code using the async/await pattern reads much more linearly. Your brain doesn’t have to jump backward in the code after reading the .perform line to see what happens next. Also, the do/try/catch area handles all the errors.

Task {
  let request = VNRecognizeTextRequest()
  let handler = VNImageRequestHandler(cgImage: image, options: [:])

  do {
    try await handler.perform([request])

    guard let observations = request.results as?
      [VNRecognizedTextObservation] else {
        throw VisionError.noResults
    }

    let recognizedText = observations.compactMap
      { $0.topCandidates(1).first?.string }
    print("Recognized Text: \(recognizedText)")
  } catch {
    print("Error recognizing text: \(error.localizedDescription)")
  }
}

Simulator Compute Devices

You’ll remember that in the earlier examples, you added the following snippet to get any Vision framework code to work with the simulator:

#if targetEnvironment(simulator)
  request.usesCPUOnly = true
#endif

That code forced the request to execute on the CPU and most of the framework code worked then. For some request types, you got simulated results on a simulator, but that’s a different issue, at least you weren’t getting errors.

In iOS 18, Apple reminds you that it really doesn’t want you running Vision requests on the simulator and have deprecated the .usesCPUOnly property of a request. However, there are legitimate reasons you might want to run Vision code on a simulator - unit tests being one of the big ones. You can use this slightly more complicated code to query for all the supported compute devices, and then figure out which is the CPU and run on that.

#if targetEnvironment(simulator)
if let supportedDevices = try? request.supportedComputeStageDevices {
  if let mainStage = supportedDevices[.main] {
    if let cpuDevice = mainStage.first(where: { device in
      device.description.contains("CPU") }) {
      request.setComputeDevice(cpuDevice, for: .main)
    }
  }
}
#endif

This code queries the system for all the supported devices that can execute Vision requests. Then, it looks for one that has “CPU” in its description and uses that one to execute the request. The odd-looking format of device.description.contains("CPU") uses the String device description to search for “CPU”. Any time you’ve got code that is querying the description of an object, you know it’s not a solution you want in your production code. Thankfully, when you’re following Apple’s advice and using only a physical device, it’s not needed.

See forum comments
Download course materials from Github
Previous: Instruction 01 Next: Demo