运行后台上下文核心数据任务后,当更新在 SwiftUI 视图中发布时,Xcode 会显示以下紫色运行时警告:
"[SwiftUI] Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates."
除了下面的
ContentView.swift
代码之外,我还在默认的container.viewContext.automaticallyMergesChangesFromParent = true
代码中将init
添加到了Persistence.swift
。
如何在主线程上发布后台更改来修复警告? (iOS 14、Swift 5)
编辑:为了响应第一个答案,我更改了下面的代码,以澄清我正在寻找一种在保存大量更改时不会阻塞 UI 的解决方案。
struct PersistenceHelper {
private let context: NSManagedObjectContext
init(context: NSManagedObjectContext = PersistenceController.shared.container.viewContext) {
self.context = context
}
public func fetchItem() -> [Item] {
do {
let request: NSFetchRequest<Item> = Item.fetchRequest()
var items = try self.context.fetch(request)
if items.isEmpty { // Create items if none exist
for _ in 0 ..< 250_000 {
let item = Item(context: context)
item.timestamp = Date()
item.data = "a"
}
try! context.save()
items = try self.context.fetch(request)
}
return items
} catch { assert(false) }
}
public func updateItemTimestamp(completionHandler: @escaping () -> ()) {
PersistenceController.shared.container.performBackgroundTask({ backgroundContext in
let start = Date(), request: NSFetchRequest<Item> = Item.fetchRequest()
do {
let items = try backgroundContext.fetch(request)
for item in items {
item.timestamp = Date()
item.data = item.data == "a" ? "b" : "a"
}
try backgroundContext.save() // Purple warning appears here
let interval = Double(Date().timeIntervalSince(start) * 1000) // Artificial two-second delay so cover view has time to appear
if interval < 2000 { sleep(UInt32((2000 - interval) / 1000)) }
completionHandler()
} catch { assert(false) }
})
}
}
// A cover view with an animation that shouldn't be blocked when saving the background context changes
struct CoverView: View {
@State private var toggle = true
var body: some View {
Circle()
.offset(x: toggle ? -15 : 15, y: 0)
.frame(width: 10, height: 10)
.animation(Animation.easeInOut(duration: 0.25).repeatForever(autoreverses: true))
.onAppear { toggle.toggle() }
}
}
struct ContentView: View {
@State private var items: [Item] = []
@State private var showingCoverView = false
@State private var refresh = UUID()
let persistence = PersistenceHelper()
let formatter = DateFormatter()
var didSave = NotificationCenter.default
.publisher(for: .NSManagedObjectContextDidSave)
// .receive(on: DispatchQuene.main) // Doesn't help
var body: some View {
ScrollView {
LazyVStack {
Button("Update Timestamp") {
showingCoverView = true
persistence.updateItemTimestamp(completionHandler: { showingCoverView = false })
}
ForEach(items, id: \.self) { item in
Text(formatter.string(from: item.timestamp!) + " " + (item.data ?? ""))
}
}
}
.id(refresh)
.onAppear {
formatter.dateFormat = "HH:mm:ss"
items = persistence.fetchItem()
}
.onReceive(didSave) { _ in
items = persistence.fetchItem()
}
.fullScreenCover(isPresented: $showingCoverView) {
CoverView().onDisappear { refresh = UUID() }
}
}
}
由于您正在执行后台任务,因此您位于后台线程 - 而不是主线程。
要切换到主线程,请将生成运行时警告的行更改为以下内容:
DispatchQueue.main.async {
try backgroundContext.save()
}
您应该使用组合并观察背景上下文的变化,并更新状态值以便您的 UI 做出反应。
@State private var coreDataAttribute = ""
var body: some View {
Text(coreDataAttribute)
.onReceive(
CoreDataManager.shared.moc.publisher(for: \.hasChanges)
.subscribe(on: DispatchQueue.global())
.receive(on: DispatchQueue.global())
.map{_ in CoreDataManager.shared.fetchCoreDataValue()}
.filter{$0 != coreDataAttribute}
.receive(on: DispatchQueue.main))
{ value in
coreDataAttribute = value
}
}
问题在于:
⠇
var didSave = NotificationCenter.default
.publisher(for: .NSManagedObjectContextDidSave)
⠇
.onReceive(didSave)
…接收应用程序中
所有 CoreData 上下文的
NSManagedObjectContextDidSave
。保存后台上下文时,SwiftUI 会在后台线程上收到通知并发出抱怨。然后,当 CoreData 更改合并到 UI 上下文中时,SwiftUI 再次收到通知,一切都很好。
要解决此问题,只需观察 UI 代码使用的上下文的通知:
.onReceive(NotificationCenter.default.publisher(for: .NSManagedObjectContextDidSave, object: context)) {