实例方法“wait”在异步上下文中不可用;使用任务组代替;这是 Swift 6 中的错误

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

具有以下代码并继续收到此错误消息。

var mediaimage = Image("blank")

let imageManager =  PHImageManager()

group.enter()
imageManager.requestImage(for: asset, targetSize: imageSize, contentMode: .aspectFill, options: requestImageOptions, resultHandler: { (image, info) in
 if image != nil {
  mediaimage = Image(uiImage: image!)
  group.leave()
 } else {
  group.leave()
 }
})

group.wait()

已尝试使用 Task{} ,其他错误显示使用 Xcode 15 构建的 iOS 16。如果我不执行 group.enter() ,则图像不会添加到 mediaimage 变量中。

ios swift image request
1个回答
0
投票

使用遗留的

wait
函数(无论是调度组还是信号量或者其他什么)是一种反模式。如果从主线程调用,问题可能包括 UI 故障到应用程序灾难性终止。即使从后台线程调用,这也是一个坏主意,因为它会阻塞工作线程池中数量非常有限的线程之一,这不仅效率低下,而且如果耗尽该池,可能会导致更严重的问题。

错误消息中提到的“使用任务组”有点误导。他们假设,如果您使用的调度组用于其预期目的,即管理任务的group,其中

TaskGroup
是现代的替代方案。

但是您没有一组任务,而只有一个任务。所以我们不会使用任务组。相反,我们只需将遗留异步 API 包装在

withCheckedThrowingContinuation
:

extension PHImageManager {
    func image(
        for asset: PHAsset,
        targetSize: CGSize,
        contentMode: PHImageContentMode = .default,
        options: PHImageRequestOptions? = nil
    ) async throws -> UIImage {
        assert(!(options?.isSynchronous ?? false))

        return try await withCheckedThrowingContinuation { continuation in
            requestImage(for: asset, targetSize: targetSize, contentMode: contentMode, options: options) { image, _ in
                if let image {
                    continuation.resume(returning: image)
                } else {
                    continuation.resume(throwing: ImageManagerError.noImage)
                }
            }
        }
    }
}

extension PHImageManager {
    enum ImageManagerError: Error {
        case noImage
    }
}

那么你可以这样做:

func fetch(asset: PHAsset, imageSize: CGSize) async throws -> Image {
    let uiImage = try await imageManager.image(for: asset, targetSize: imageSize)
    return Image(uiImage: uiImage)
}

通过遵循

async
-
await
模式,我们可以避免调用旧版
wait
API,从而避免阻塞线程。


虽然我试图保持示例的简单性,但有两个注意事项:

  1. 像您原来的示例一样,它不处理取消。

    您可以修改上面的内容以通过将其包装在

    withTaskCancellationHandler
    :

    中来处理取消
    extension PHImageManager {
        func image(
            for asset: PHAsset,
            targetSize: CGSize,
            contentMode: PHImageContentMode = .default,
            options: PHImageRequestOptions? = nil
        ) async throws -> UIImage {
            assert(!(options?.isSynchronous ?? false))
    
            let request = ImageRequest(manager: self)
    
            return try await withTaskCancellationHandler {
                try await withCheckedThrowingContinuation { continuation in
                    guard !request.isCancelled else {
                        continuation.resume(throwing: CancellationError())
                        return
                    }
    
                    request.id = requestImage(for: asset, targetSize: targetSize, contentMode: contentMode, options: options) { image, _ in
                        if let image {
                            continuation.resume(returning: image)
                        } else {
                            continuation.resume(throwing: ImageManagerError.noImage)
                        }
                    }
                }
            } onCancel: {
                request.cancel()
            }
        }
    }
    
    private extension PHImageManager {
        class ImageRequest: @unchecked Sendable {
            private weak var manager: PHImageManager?
            private let lock = NSLock()
            private var _id: PHImageRequestID?
            private var _isCancelled = false
    
            init(manager: PHImageManager) {
                self.manager = manager
            }
    
            var id: PHImageRequestID? {
                get { lock.withLock { _id } }
                set { lock.withLock { _id = newValue } }
            }
    
            var isCancelled: Bool {
                get { lock.withLock { _isCancelled } }
            }
    
            func cancel() {
                lock.withLock {
                    _isCancelled = true
    
                    if let id = _id {
                        manager?.cancelImageRequest(id)
                    }
                }
            }
        }
    }
    
  2. 有时

    requestImage
    会多次调用其完成处理程序闭包(除非您使用
    deliveryMode
    highQualityFormat
    fastFormat
    )。

    就像您的调度组示例一样,

    withCheckedContinuation
    要求您
    resume
    恰好一次且仅一次。如果我们想要支持多个图像(例如,检索本地低质量图像并随后检索远程高质量图像),我们将使用
    AsyncSequence
    ,即
    AsyncStream
    :

    extension PHImageManager {
        func images(
            for asset: PHAsset,
            targetSize: CGSize,
            contentMode: PHImageContentMode = .default,
            options: PHImageRequestOptions? = nil
        ) -> AsyncStream<UIImage> {
            assert(!(options?.isSynchronous ?? false))
    
            let request = ImageRequest(manager: self)
    
            return AsyncStream { continuation in
                request.id = requestImage(for: asset, targetSize: targetSize, contentMode: contentMode, options: options) { image, info in
                    if let image {
                        continuation.yield(image)
                    }
    
                    // don't finish, yet, if current result is degraded (and we didn't ask for `fastFormat`)
    
                    guard
                        let isDegraded = info?[PHImageResultIsDegradedKey] as? Bool,
                        isDegraded,
                        options?.deliveryMode != .fastFormat
                    else {
                        return
                    }
    
                    // otherwise, go ahead and finish
    
                    continuation.finish()
                }
    
                continuation.onTermination = { reason in
                    guard case .cancelled = reason else { return }
    
                    request.cancel()
                }
            }
        }
    }
    

    然后你会做类似的事情:

    func fetchImages(for asset: PHAsset, imageSize: CGSize) async {
        for await uiImage in imageManager.images(for: asset, targetSize: imageSize) {
            let image = Image(uiImage: uiImage)
            // do something with `image`
        }
    }
    

    我必须承认,我发现上面的“完成了吗”逻辑有点脆弱。 (Apple怎么可能不提供一个简单的属性来检查请求是否完成呢?)

我承认我只是快速地将其组合在一起,并没有进行任何详尽的测试,但希望它说明了一些与将遗留异步 API 包装在 Swift 并发模式中并避免调用遗留

wait
函数相关的概念。

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