使用 Swift Concurrency 将 [NSItemProvider] 内容的顺序访问转换为并行访问

问题描述 投票:0回答:1

我正在制作一个 Action Extension,它接收远程图像的图像和 URL 的混合,然后在返回 UIImage 数组之前对它们进行一些图像处理。

启动时,我使用系统提供的

loadImages
数组来调用该处理程序类的
NSExtensionItem
方法:

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
    }
}

这可行,但它按顺序处理事物,访问每个

NSItemProvider
内的 URL,然后加载其数据并从中创建图像。我想将其并行化,因为资产经过处理的顺序并不重要。我尝试使用
TaskGroup
,但是当我尝试添加子任务来调用
load
方法时,我收到一条警告,指出我正在捕获不可发送类型
NSItemProvider
。我不知道这有多危险,但理想情况下我想做一些不会产生警告的事情。

另外,如果我想要真正的并发,我可能不应该标记此类

@MainActor
,但我不确定如何将结果返回到主线程以更新将显示最终结果的图像视图。

swift ios-app-extension swift-concurrency
1个回答
0
投票

可悲的事实是,他们根本没有更新/审核 Swift 并发基础的大部分内容。就我个人而言,我不愿意对线程安全性或可发送性做出假设,所以当使用尚未与 Swift 并发很好地配合的 API 时,我倾向于遵循遗留模式(我会保留

private
),但随后公开一个
async
函数将遗留接口包装在延续中。例如,

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()
        }
    }
}

我承认有人可能会说这种方法过于谨慎,但恕我直言,这是谨慎的。

注意,我还没有测试上述内容,所以如果我引入任何错误,我很抱歉,但希望它说明了这个想法。

© www.soinside.com 2019 - 2024. All rights reserved.