【问题标题】:Publish background context Core Data changes in a SwiftUI view without blocking the UI在 SwiftUI 视图中发布后台上下文 Core Data 更改而不阻塞 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() }
        }
    }
}

【问题讨论】:

    标签: core-data swiftui publish combine background-thread


    【解决方案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
            }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-10-04
      • 2016-10-18
      • 2014-08-30
      • 2020-12-19
      • 2022-09-28
      • 1970-01-01
      相关资源
      最近更新 更多