有下面的代码,并继续得到这个错误消息.

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

我try 了任务{},但其他错误显示iOS 16 Build with Xcode 15.如果我不执行group.enter(),则不会将映像添加到Mediaimage变量.

推荐答案

使用遗留的wait函数(无论是分派组还是信号量或您拥有的任何东西)是一种反模式.如果从主线程调用,问题可能从UI中的故障到应用程序的灾难性终止.即使从后台线程调用,它也不是一个好主意,因为它会阻塞辅助线程池中数量非常有限的线程之一,这既效率低下,而且如果耗尽该池,可能会导致更严重的问题.此外,在异步上下文中不允许wait,因为SWIFT并发性依赖于必须允许所有线程"向前进展"(即,从不阻塞)的合同.

不幸的是,错误消息中提到的"使用一个任务组代替"有点误导.他们假设,如果您使用的是一个调度组,您正在使用它的预期目的,即管理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), "Synchronous image retrieval not permitted from Swift concurrency")

        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: (info?[PHImageErrorKey] as? Error) ?? 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)
}

通过遵循asyncawait模式,我们避免调用遗留的wait API,从而避免阻塞线程.


虽然我试图保留你的例子的简单性,但有两个警告:

  1. 就像你最初的例子一样,上面的内容不处理取消.但是在编写Swift并发代码时,如果底层API支持取消(例如requestImage),我们总是希望支持取消.

    您可以修改上面的代码以处理取消,方法是将其包含在withTaskCancellationHandler中:

    extension PHImageManager {
        func image(
            for asset: PHAsset,
            targetSize: CGSize,
            contentMode: PHImageContentMode = .default,
            options: PHImageRequestOptions? = nil
        ) async throws -> UIImage {
            assert(!(options?.isSynchronous ?? false), "Synchronous image retrieval not permitted from Swift concurrency")
    
            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: (info?[PHImageErrorKey] as? Error) ?? 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会多次调用它的完成处理程序闭包(除非你使用highQualityFormatfastFormat中的deliveryMode).

    就像您的调度组示例一样,withCheckedContinuation要求您resume只执行一次且仅执行一次.如果我们想支持多个图像(例如,本地低质量图像的检索和远程高质量图像的后续检索),我们将使用AsyncSequence,即AsyncStream:

    extension PHImageManager {
        func images(
            for asset: PHAsset,
            targetSize: CGSize,
            contentMode: PHImageContentMode = .default,
            options: PHImageRequestOptions? = nil
        ) -> AsyncThrowingStream<UIImage, Error> {
            assert(!(options?.isSynchronous ?? false), "Synchronous image retrieval not permitted from Swift concurrency")
    
            let request = ImageRequest(manager: self)
    
            return AsyncThrowingStream { continuation in
                request.id = requestImage(for: asset, targetSize: targetSize, contentMode: contentMode, options: options) { image, info in
                    guard let image else {
                        continuation.finish(throwing: (info?[PHImageErrorKey] as? Error) ?? ImageManagerError.noImage)
                        return
                    }
    
                    continuation.yield(image)
    
                    // don't finish, yet, if current result is degraded (and we didn't ask for `fastFormat`)
    
                    if
                        let isDegraded = info?[PHImageResultIsDegradedKey] as? Bool,
                        isDegraded,
                        options?.deliveryMode != .fastFormat
                    {
                        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 throws {
        for try await uiImage in imageManager.images(for: asset, targetSize: imageSize) {
            let image = Image(uiImage: uiImage)
            // do something with `image`
        }
    }
    

    我必须承认,我发现上面的"完成了吗"逻辑有点脆弱.(Apple怎么能不提供一个简单的属性来判断请求是否完成以及完成处理程序不会再次被调用?)但是,你明白了.

我承认,我只是快速地把它放在一起,并没有做详尽的测试,但是,希望它能说明一些与在Swift并发模式中包装遗留异步API以及避免调用遗留wait函数相关的概念.

Ios相关问答推荐

通过拖动更改视图位置的SwiftUI会引发UI错误

当包含UITextView的inputAccessoryView显示键盘时,iOS UITableView内容会出现UINavigationBar奇怪的错误

AVFoundation Camera推出变焦SWIFT

一堆UIGraphics方法在iOS 17中被弃用,没有明确的替代方法?

如何在不将应用程序留在SwiftUI上的情况下更改语言?

使用 Foundation 解压缩文件

Flutter iOS 构建失败并在 ios/Runner/AppDelegate.swift 中出现多个错误

SwiftUI - ScrollView 不显示底部文本

Swiftui为什么在点击第二行之前背景不起作用

满足条件时 SwiftUI 动画背景 colored颜色 变化

如何判断 `NSManagedObject` 是否已被删除?

CocoaPods 找不到 podFirebase/Core的兼容版本 | cloud_firestore, Flutter

每次运行后,Xcode 6 都会在 iOS8 模拟器中重命名我的应用程序目录.

未找到签名证书​​iOS Distribution

在 Swift 中上传带参数的图像

在 Swift 中禁用向后滑动手势

关闭通过模态 segue 显示的视图

iOS 7 半透明模态视图控制器

UINavigationBar - 以编程方式设置标题?

我可以在 UIScrollView 中使用 UIRefreshControl 吗?