在 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 和 Core Motion

SwiftUI - 将托管对象上下文传递给模型

SwiftUI:表达式类型不明确,没有更多上下文,在视图之间传递 ObservableObject 时 [重复]

Core Data 更改时更新 Swiftui 视图

SwiftUI:如何将 NSManagedObjectContext 传递到多个视图模型中

从 Core Data 非标量属性更新 SwiftUI 视图