让 async/await 与 ObservableObject、Published、StateObject 一起使用

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

让我说,我是在 SwiftUI 中使用 UI 的新手,因此,如果在其他各种情况下可能已对此问题进行了相邻回答,我深表歉意,但我似乎找不到特定于我的问题的问题。

我创建了一个为 MacOS 生成 CGImage 的函数。它在我最初打算使用的命令行工具中运行得非常好。无需赘述,它是异步的,因为它使用 withTaskGroup() 来减少生成图像所需的工作时间。

我想在 @ObservableObject 类中使用它的 @Published var。这是我从非异步版本开始的基本代码:

struct ContentView: View {
    @StateObject var target = Target()
    
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
            Button("Hello, world! \(target.value)")
            {
                target.value += 1
                if (target.value == 3)
                {
                    target.value = 0
                }
                target.update()
            }
            target.image
        }
        .padding()
    }
}

class Target: ObservableObject
{
    @Published var image: Image
    @Published var value: Int
    
    init()
    {
        value = 0
        image = Target.update(input: 0)        
    }
    
    func update()
    {
        image = Target.update(input: value)
    }
    
    static func update(input: Int) -> Image
    {
        var cgimage: CGImage?
        cgimage = drawMyImage(input: input)
        return Image(cgimage!, scale: 1.0, label: Text("target \(input)"))        
    }
}

如果有人对快速测试感兴趣,我在下面放置了一个虚拟的drawMyImage()函数。

所以上面的代码不是异步的。问题是当我开始使用异步时,所以我异步了drawMyImage()函数,这就是我真正的drawImage()函数:

func drawMyImage(input: Int) async -> CGImage?

尝试1

不支持并发的自动闭包中的“异步”调用

如果我在堆栈上插入适当的异步/等待:

    init() async
    {
        value = 0
        image = await Target.update(input: 0)
        
    }
    
    func update() async
    {
        image = await Target.update(input: value)
    }
    
    static func update(input: Int) async -> Image
    {
        var cgimage: CGImage?
        cgimage = await drawMyImage(input: input)

我最终在 ContentView 中得到了这个我似乎无法解决的问题:

struct ContentView: View {
    @StateObject var target = Target() //'async' call in an autoclosure that does not support concurrency
    
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
            Button("Hello, world! \(target.value)")
            {
                target.value += 1
                if (target.value == 3)
                {
                    target.value = 0
                }
                target.update() //'async' call in an autoclosure that does not support concurrency

尝试2

并发执行代码中捕获的 var 'myImage' 发生变化

所以我尝试仅将drawMyImage()函数包装在任务中,但效果不太好:

    static func update(input: Int) -> Image
    {
        var myImage: Image
        Task
        {
            var cgimage: CGImage?
            cgimage = await drawMyImage(input: input)
            myImage = Image(cgimage!, scale: 1.0, label: Text("target \(input)")) //Mutation of captured var 'myImage' in concurrently-executing code
        }
        return myImage
    }

尝试3

在同步非隔离上下文中调用主参与者隔离的静态方法“update(input:)”

所以我尝试添加 @MainActor 装饰器,这给了我一个不同的错误:

    init()
    {
        value = 0
        image = Target.update(input: 0) //Call to main actor-isolated static method 'update(input:)' in a synchronous nonisolated context
        
    }
    
    func update()
    {
        image = Target.update(input: value) //Call to main actor-isolated static method 'update(input:)' in a synchronous nonisolated context
    }
    
    @MainActor
    static func update(input: Int) -> Image
    {

我已经尝试了其他几件事来解决这个问题,但这似乎是我一遍又一遍地解决的主要问题。如果有人能帮我一把,我将不胜感激。预先感谢。

虚拟drawMyImage()函数

//dummy test function with no real input, no real async
//remove 'async' for how it's supposed to work
func drawMyImage(input: Int) async -> CGImage? {
    let bounds = CGRect(x: 0, y:0, width: 200, height: 200)
    let intWidth = Int(ceil(bounds.width))
    let intHeight = Int(ceil(bounds.height))
    let bitmapContext = CGContext(data: nil,
                                  width: intWidth, height: intHeight,
                                  bitsPerComponent: 8,
                                  bytesPerRow: (((intWidth * 4) + 15) / 16) * 16,
                                  space: CGColorSpace(name: CGColorSpace.sRGB)!,
                                  bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue)
    if let cgContext = bitmapContext {
        cgContext.saveGState()
        if input == 0
        {
            cgContext.setFillColor(red: 255, green: 0, blue: 0, alpha: 255)
        }
        else if input == 1
        {
            cgContext.setFillColor(red: 0, green: 255, blue: 0, alpha: 255)
        }
        else
        {
            cgContext.setFillColor(red: 0, green: 0, blue: 255, alpha: 255)
        }
        cgContext.fill(bounds)
        cgContext.restoreGState()

        return cgContext.makeImage()
    }

    return nil
}
macos swiftui async-await observedobject
1个回答
0
投票

就像第一次尝试一样,您可以在调用链中传播

async
,但不能直到
init
。因为该操作是异步的,所以
Target
不会 在初始化后立即拥有可用的
Image
。另外,正如您在尝试中发现的那样,该视图需要
ObservableObject
同步初始化。

您应该将

image
设为可选:

class Target: ObservableObject
{
    @Published var image: Image?
    @Published var value: Int = 0
    
    @MainActor // isolate this to the main actor because @Published vars can only be updated from the main thread
    func update() async
    {
        image = await Target.update(input: value)
    }
    
    static func update(input: Int) async -> Image
    {
        var cgimage: CGImage?
        cgimage = await drawMyImage(input: input)
        return Image(cgimage!, scale: 1.0, label: Text("target \(input)"))
    }
}

在您看来,这是使用

task(id:)
的绝佳机会。当视图首次出现以及其
id
参数更改时,这会运行异步操作。只需传递
target.value
作为 ID,SwiftUI 就会为您处理所有任务取消。

VStack {
    Image(systemName: "globe")
        .imageScale(.large)
        .foregroundStyle(.tint)
    Button("Hello, world! \(target.value)")
    {
        target.value += 1
        if (target.value == 3)
        {
            target.value = 0
        }
    }
    target.image
}
.padding()
.task(id: target.value) {
    await target.update()
}
© www.soinside.com 2019 - 2024. All rights reserved.