在 SwiftUI 视图中发布后台上下文 Core Data 更改而不阻塞 UI
Posted
技术标签:
【中文标题】在 SwiftUI 视图中发布后台上下文 Core Data 更改而不阻塞 UI【英文标题】:Publish background context Core Data changes in a SwiftUI view without blocking the UI 【发布时间】:2021-09-21 19:56:01 【问题描述】:运行后台上下文核心数据任务后,当更新在 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
代码之外,我还在默认的Persistence.swift
代码中将container.viewContext.automaticallyMergesChangesFromParent = true
添加到init
。
如何在主线程上发布背景更改以修复警告? (ios 14,斯威夫特 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()
【问题讨论】:
【参考方案1】:由于您正在执行后台任务,因此您处于后台线程 - 而不是主线程。
要切换到主线程,请将产生运行时警告的行更改为以下内容:
DispatchQueue.main.async
try backgroundContext.save()
【讨论】:
它在我的示例代码中没有很好地表示,但我使用后台上下文来避免阻塞主线程上的 UI,当有很多更改时,此解决方案会阻塞 UI保存。有没有办法从后台上下文中保存并在之后发布或通知主线程? 顺便说一句,这打破了人为的等待,在所有条件下都保持两秒钟。 @jesseblake 问题是(它出现)当你保存你的 CoreData 东西时,有些东西正在从后台线程更新 SwiftUI@State
、@Binding
、@Publisher
等变量。你能显示PersistenceController.shared.container.performBackgroundTask
方法吗?编辑您的问题以包含它。 PersistenceController
模型中是否可能有 @FetchRequest
?改为包含所有内容可能会很有用。
我向Item
实体添加了String
属性,代码现在创建250_000
项目。在我的 Mac 上更新大约需要 5 秒钟。我还在封面视图中添加了一个动画,以显示在后台线程中进行保存时原始代码不会阻塞 UI。
我可以将try backgroundContext.save()
和completionHandler()
放入DispatchQueue.main.async
,但保存时UI 被阻止。【参考方案2】:
您应该使用组合并观察背景上下文的变化并更新状态值以使您的 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
【讨论】:
以上是关于在 SwiftUI 视图中发布后台上下文 Core Data 更改而不阻塞 UI的主要内容,如果未能解决你的问题,请参考以下文章
SwiftUI:表达式类型不明确,没有更多上下文,在视图之间传递 ObservableObject 时 [重复]