I'm making an Action Extension that takes in a mix of images and URLs for remote images and then does some image processing on them before returning an array of UIImage.
At startup, I'm calling the loadImages
method of this handler class with the array of NSExtensionItem
provided by the system:
import UniformTypeIdentifiers
import UIKit
@MainActor
public final class ImageImporter {
public enum Error: Swift.Error {
case itemsHadNoImages
case failedToLoadImage
}
public func loadImages(from items: [NSExtensionItem]) async throws -> [UIImage] {
let itemProviders = items
.compactMap { $0.attachments }
.flatMap { $0 }
let imageProviders = itemProviders.filter { $0.hasItemConformingToTypeIdentifier(UTType.image.identifier) }
let urlProviders = itemProviders.filter { $0.hasItemConformingToTypeIdentifier(UTType.url.identifier) }
var images: [UIImage] = []
for provider in imageProviders {
do {
let image = try await load(provider: provider, type: UTType.image)
images.append(image)
} catch {
continue
}
}
for provider in urlProviders {
do {
let image = try await load(provider: provider, type: UTType.url)
images.append(image)
} catch {
continue
}
}
guard !images.isEmpty else { throw Error.itemsHadNoImages }
return images
}
private func load(provider: NSItemProvider, type: UTType) async throws -> UIImage {
let result = try await provider.loadItem(forTypeIdentifier: type.identifier)
guard let url = result as? URL else { throw Error.itemsHadNoImages }
let (data, _) = try await URLSession.shared.data(from: url)
guard let image = UIImage(data: data) else { throw Error.failedToLoadImage }
return image
}
}
This works, but it processes things sequentially, accessing the URL inside each NSItemProvider
and then loading its data and creating an image from it. I would like to parallelize this since the order that the assets go through processing doesn't matter. I tried using a TaskGroup
, but when I try to add a sub-task to call the load
method, I get a warning that I'm capturing a non-Sendable type NSItemProvider
. I have no idea how dangerous this is, but ideally I'd want to be doing something that doesn't produce warnings.
Also, if I want true concurrency, I probably shouldn't mark this class @MainActor
, but I'm not sure how to get the results back onto the main thread for updating the image views that will display the final results.
The sad fact is that they simply have not updated/audited much of Foundation for Swift concurrency. Personally, I am reluctant to make assumptions regarding thread-safety or sendability, so I would be inclined to follow legacy patterns (which I would keep private
) when using API that does not yet play well with Swift concurrency, but then expose an async
function that wraps the legacy interface in a continuation. E.g.,
import UIKit
import UniformTypeIdentifiers
import os.log
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "ImageImporter")
@MainActor
public final class ImageImporter {
public enum Error: Swift.Error {
case itemsHadNoImages
case failedToLoadImage
}
public func loadImages(from items: [NSExtensionItem]) async throws -> [UIImage] {
try await withCheckedThrowingContinuation { continuation in
loadImages(from: items) { result in
continuation.resume(with: result)
}
}
}
}
private extension ImageImporter {
func loadImages(from items: [NSExtensionItem], completion: @MainActor @Sendable @escaping (Result<[UIImage], Error>) -> Void) {
let permissibleTypes: [UTType] = [.image, .url]
let providersAndTypes = items
.compactMap { $0.attachments }
.flatMap { $0 }
.compactMap {
for type in permissibleTypes {
if $0.hasItemConformingToTypeIdentifier(type.identifier) {
return ($0, type)
}
}
return nil
}
let group = DispatchGroup()
var images: [UIImage] = []
for (provider, type) in providersAndTypes {
group.enter()
loadImage(provider: provider, type: type) { result in
defer { group.leave() }
switch result {
case .success(let image): images.append(image)
case .failure(let error): logger.error("\(#function): \(error)")
}
}
}
group.notify(queue: .main) {
guard !images.isEmpty else {
let error = Error.itemsHadNoImages
logger.warning("\(#function): \(error)")
completion(.failure(error))
return
}
completion(.success(images))
}
}
func loadImage(provider: NSItemProvider, type: UTType, completion: @MainActor @Sendable @escaping (Result<UIImage, Swift.Error>) -> Void) {
provider.loadItem(forTypeIdentifier: type.identifier) { url, error in
guard error == nil, let url = url as? URL else {
DispatchQueue.main.async {
completion(.failure(error ?? Error.itemsHadNoImages))
}
return
}
URLSession.shared.dataTask(with: url) { data, _, error in
guard error == nil, let data, let image = UIImage(data: data) else {
DispatchQueue.main.async {
completion(.failure(error ?? Error.failedToLoadImage))
}
return
}
DispatchQueue.main.async {
completion(.success(image))
}
}.resume()
}
}
}
I admit that one could argue that this approach is overly cautious, but is prudent, IMHO.
Note, I have not tested the above, so I apologize if I introduced any errors, but hopefully it illustrates the idea. But, as shown above, you can enjoy parallelism with the legacy completion handler pattern, and use the dispatch group to know when they are done.