英文:
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()
}
}
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论