How to update Core Data and then UI via a background operation from a button press using Swift Async/Await in SwiftUI

huangapple go评论66阅读模式
英文:

How to update Core Data and then UI via a background operation from a button press using Swift Async/Await in SwiftUI

问题

我已经阅读了关于这个主题的其他问题和答案,但似乎找不到在我的情况下使这个工作的解决方案。我的具体情况是,我有一个按钮,用户按下该按钮来启动一些异步工作,比如一个API调用。在完成这个异步工作后,需要更新Core Data对象,以及引用该对象的UI,以使用新检索到的数据,但我无法弄清楚需要设置的确切方式。以下是我当前的尝试:

@ObservedObject data: MyDataObject // 从上一视图传入的Core Data实体
let managedObjectContext = DataController.shared.context // Core Data NSPersistentContainer单例

var body: some View {
  VStack {
    Text(data.info)

    Button("Tap") {
      getResponseFromNetworkAPI(using: data)
    }
  }
}

func getResponseFromNetworkAPI(using data: MyDataObject) {
  // 在后台执行的工作。一旦获取响应,我希望在MainActor上更新我的Core Data实体(我认为这是最佳实践)。
  Task.detached(priority: .userInitiated) {
    var response: String? = nil
    response = await APIServiceClass.requestResponse(using: data)

    await MainActor.run {
       // 在下面的代码行上出现错误:
       // "在并发执行的代码中引用捕获变量'response'"
       data.info = response
       try? managedObjectContext.save()
    }
  }
}

我的直觉是我可以从后台任务中安排一个MainActor任务,但我不确定如何正确传递数据给它,因为我不被允许在后台任务中引用检索到的数据。可能有一种修复我的特定设置的方法,但我也对如何最佳实践地执行此操作感到好奇。

APIServiceClass.requestResponse(using:)函数是一个从某个网络调用返回String?的异步函数。

英文:

I've read other questions and answers on this topic but can't seem to find a solution to make this work in my situation. My specific situation is I have a button that the user presses to kick-off some async work, like an API call. On completion of this async work a Core Data object, and thus the UI that is referencing that object, needs to be updated with the newly retrieved data, but I can't figure out the exact way this needs to be set up. Below is my current attempt:

@ObservedObject data: MyDataObject // Core Data entity passed in from an upper view
let managedObjectContext = DataController.shared.context // Core Data NSPersistentContainer singleton

var body: some View {
  VStack {
    Text(data.info)

    Button("Tap") {
      getResponseFromNetworkAPI(using: data)
    }
  }
}

func getResponseFromNetworkAPI(using data: MyDataObject) {
  // Do work that should be in the background. Once the response is fetched, I want to
  // update my Core Data entity on the MainActor (which I believe is best practice).
  Task.detached(priority: .userInitiated) {
    var response: String? = nil
    response = await APIServiceClass.requestResponse(using: data)

    await MainActor.run {
       // Error here on the below line:
       // "Reference to capture var 'response' in concurrently-executing code"
       data.info = response
       try? managedObjectContext.save()
    }
  }
}

My intuiton is that I can schedule a MainActor task from the background task, but I'm not sure how to pass data into it correctly, as I'm not allowed to reference the data retrieved in the background task. There may be a fix to my specific setup, but I'm also curious about a best practice way to do this.

The APIServiceClass.requestResponse(using:) function is an async function that returns a String? from some network call.

答案1

得分: 3

这是我会做的事情,请看注释。

struct SampleFlowView: View {
    @Environment(\.managedObjectContext) var context
    @ObservedObject var data: MyDataObject
    @State private var gettingResponse: Bool = false
    var body: some View {
        VStack {
            Text(data.info ?? "没有信息")
            
            Button {
                gettingResponse.toggle() // 使用按钮触发 .task
            } label: {
                Text("点击").overlay {
                    if gettingResponse {
                        ProgressView() // 在任务运行时显示进度视图
                    }
                }
            }
            .disabled(gettingResponse) // 禁用按钮以防止重复调用
            .task(id: gettingResponse) { // 异步等待任务
                guard gettingResponse else {
                    return
                }
                print("更新中")
                await getResponseFromNetworkAPI(using: data) // 运行你的方法
                
                gettingResponse = false // 让界面知道你已经完成
                print("完成")
            }
        }
    }
    
    func getResponseFromNetworkAPI(using data: MyDataObject) async {
        // 当长时间 API 调用返回时设置响应字符串
        let responseString = await Task.detached(priority: .userInitiated) {
            try? await Task.sleep(for: .seconds(2)) // 模拟延迟
            // 模拟返回一个字符串
            return "测试 \((0...100).randomElement()!)" // 替换为使用数据发出请求的实际代码
        }.value
        
        // 使用 Core Data 并发性
        await context.perform { // 使用异步等待让“actor”确定如何保存上下文
            
            data.info = responseString
            try? context.save()
        }
    }
}

请注意,我已经将双引号 " 替换为正常的引号以使代码有效。

英文:

Here is what I would do, see the comments.

struct SampleFlowView: View {
    @Environment(\.managedObjectContext) var context
    @ObservedObject var data: MyDataObject
    @State private var gettingResponse: Bool = false
    var body: some View {
        VStack {
            Text(data.info ?? "no info")
            
            Button {
                gettingResponse.toggle() //Use the Button to trigger a .task
            } label: {
                Text("Tap").overlay {
                    if gettingResponse {
                        ProgressView() //Show a Progress View when the task is running
                    }
                }
            }
            .disabled(gettingResponse) //Disable the button to prevent duplicate calls
            .task(id: gettingResponse) { // async await task
                guard gettingResponse else {
                    return
                }
                print("updating")
                await getResponseFromNetworkAPI(using: data) //Run your method
                
                gettingResponse = false //Let the UI know you ae done
                print("done")
            }
        }
    }
    
    func getResponseFromNetworkAPI(using data: MyDataObject) async {
        //Set the Response String when the long APi call returns
        let responseString = await Task.detached(priority: .userInitiated) {
            try? await Task.sleep(for: .seconds(2)) //Mimicking a delay
            //Mimic returning a String
            return "test \((0...100).randomElement()!)"//await APIServiceClass.requestResponse(using: data)
        }.value
        
        //Use Core Data Concurrency
        await context.perform { //Use async await to let the "actor" determine how to save the context
            
            data.info = responseString
            try? context.save()
        }
    }
}

huangapple
  • 本文由 发表于 2023年6月8日 02:00:15
  • 转载请务必保留本文链接:https://go.coder-hub.com/76425953.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定