【问题标题】:Background thread Core Data object property changes doesn't reflect on UI后台线程 Core Data 对象属性更改不会反映在 UI 上
【发布时间】:2021-06-17 16:13:13
【问题描述】:

假设我想在 CoreData 的Playlist 实体中添加一个新项目并将其放入后台线程并将其推回主线程,然后将其反映在 tableView 上。好吧,该代码在没有后台线程实现的情况下运行良好。

但是当我应用下面的后台代码时,在执行createPlaylist 之后,tableView 变成了空白空间(没有任何项目出现),尽管print(self?.playlists.count) 给出了正确的行数。

在处理 GCD 时,我将一些繁重的代码放在后台队列中,并在同一个闭包中推回主队列以进行 UI 更新。但这似乎在这里不起作用,我google了一段时间,但仍然无法解决问题。

import UIKit
import CoreData

class PlayListViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
    var songs = [Song]()
    var position = 0
    
    let container = (UIApplication.shared.delegate as! AppDelegate).persistentContainer
    private var playlists = [Playlist]()

    override func viewDidLoad() {
        super.viewDidLoad()

        view.backgroundColor = UIColor(white: 1, alpha: 1)
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "playlistCell")
        configureLayout()
        getAllPlaylists()
    }

    // MARK: Core data functions
    func getAllPlaylists() {
        do {
            let context = self.container.viewContext
            playlists = try context.fetch(Playlist.fetchRequest())
            DispatchQueue.main.async { [weak self] in
                self?.tableView.reloadData()
            }
            print("count: \(playlists.count)")
//            printThreadStats()
        } catch {
            print("getAllPlaylists failed, \(error)")
        }
    }

    func createPlaylist(name: String) {
        container.performBackgroundTask { context in
            let newPlaylist = Playlist(context: context)
            newPlaylist.name = name
            
            do {
                try context.save()
                self.playlists = try context.fetch(Playlist.fetchRequest())

                DispatchQueue.main.async { [weak self] in
                    self?.tableView.reloadData()
                    print(self?.playlists.count)
                }
            } catch {
                print("Create playlist failed, \(error)")
            }
        }
    }

    // MARK: tableView data source implementation
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return playlists.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let playlist = playlists[indexPath.row]
        let cell = tableView.dequeueReusableCell(withIdentifier: "playlistCell", for: indexPath)
        cell.textLabel?.text = playlist.name
//        cell.detailTextLabel?.text = "2 songs"
        return cell
    }

    

自动生成的 fetchRequest 和属性定义

import Foundation
import CoreData


extension Playlist {

    @nonobjc public class func fetchRequest() -> NSFetchRequest<Playlist> {
        return NSFetchRequest<Playlist>(entityName: "Playlist")
    }

    @NSManaged public var name: String?

}

【问题讨论】:

    标签: swift multithreading core-data


    【解决方案1】:

    对于func getAllPlaylists() 的第一次调用,您在主线程上从viewDidLoad() 调用它。所以下面的行在主线程上执行。

    let context = self.container.viewContext
    playlists = try context.fetch(Playlist.fetchRequest())
    

    下次在createPlaylist 方法中,您将在后台上下文中执行添加播放列表任务(而不是在主线程上)。所以下面的行在后台线程上执行。

    self.playlists = try context.fetch(Playlist.fetchRequest())
    

    另请注意,我们第一次使用viewContext 获取播放列表,第二次使用backgroundContext。这种混淆会导致 UI 无法显示预期结果。

    我认为这两种方法可以简化为-

    func getAllPlaylists() {
        do {
            let context = self.container.viewContext
            playlists = try context.fetch(Playlist.fetchRequest())
                
            // DispatchQueue.main.async not necessary, we are already on main thread
            self.tableView.reloadData()
                
            print("count: \(playlists.count)")
        } catch {
            print("getAllPlaylists failed, \(error)")
        }
    }
    
    func createPlaylist(name: String) {
        container.performBackgroundTask { context in
            let newPlaylist = Playlist(context: context)
            newPlaylist.name = name
                
            do {
                try context.save()
                
                DispatchQueue.main.async { [weak self] in
                    self?.getAllPlaylists()
                }
            } catch {
                print("Create playlist failed, \(error)")
            }
        }
    }
    

    【讨论】:

    • 谢谢,如果我也将getAllPlaylists() 中的代码放在container.performBackgroundTask 后台线程中。调用 viewDidLoad 后 tableView 变为空。真奇怪。我怎样才能让这些调用使用相同的背景上下文,我认为在每个函数中调用 container.performBackgroundTask 会创建 2 个单独的上下文,对吧?
    • @ChuckZHB 问题是——为了能够在表视图中使用播放列表数据(在主/UI线程上)——你必须使用使用主/UI线程的上下文来获取它(viewContext在你的案子)。您应该考虑在 Xcode 的方案编辑器中添加-com.apple.CoreData.ConcurrencyDebug 1 标志,例如imgur.com/JJjoTjk,并确保它被选中。每次您在与上下文线程不同的线程上使用NSManagedObject 实例时,这都会使您的应用程序崩溃。是的,container.performBackgroundTask 每次调用时都会创建一个新的背景上下文实例。
    • 谢谢,我完全从互联网上重新研究了它,因为似乎只是学习一些表面的东西不足以应对 CoreData 中的多线程。我想我得到了解决方案,请在下面查看我的答案。
    【解决方案2】:

    今天经过 5 个小时的挖掘,我找到了解决方案。我想将我的解决方案和代码放在下面,因为关于“如何在 CoreData 中的队列之间传递 NSManagedObject 实例”的内容非常罕见&& 碎片化,对 SWIFT 的新手不友好。

    问题是我们想在后台线程上执行繁重的 CoreData 任务,并在前台(主线程)反映 UI 的变化。一般来说,我们需要创建一个私有队列上下文(privateMOC),并在这个私有上下文上执行繁重的CoreData任务,见下面代码。

    为了重用,我把CoreData函数分开了。

    import UIKit
    import CoreData
    
    struct CoreDataManager {
        let managedObjectContext: NSManagedObjectContext
        private let privateMOC = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
        let coreDataStack = CoreDataStack()
        
        static let shared = CoreDataManager()
        
        private init() {
            self.managedObjectContext = coreDataStack.persistentContainer.viewContext
            privateMOC.parent = self.managedObjectContext
        }
        
        func fetchAllPlaylists(completion: @escaping ([Playlist]?) -> Void) {
            privateMOC.performAndWait {
                do {
                    let playlists: [Playlist] = try privateMOC.fetch(Playlist.fetchRequest())
                    print("getAllPlaylists")
                    printThreadStats()
                    print("count: \(playlists.count)")
                    completion(playlists)
                } catch {
                    print("fetchAllPlaylists failed, \(error), \(error.localizedDescription)")
                    completion(nil)
                }
            }
        }
        
        func createPlaylist(name: String) {
            privateMOC.performAndWait {
                let newPlaylist = Playlist(context: privateMOC)
                newPlaylist.name = name
                synchronize()
            }
        }
        
        func deletePlaylist(playlist: Playlist) {
            privateMOC.performAndWait {
                privateMOC.delete(playlist)
                synchronize()
            }
        }
    
        func updatePlaylist(playlist: Playlist, newName: String) {
            ...
        }
        
        func removeAllFromEntity(entityName: String) {
            ...
        }
        
        func synchronize() {
            do {
                // We call save on the private context, which moves all of the changes into the main queue context without blocking the main queue.
                try privateMOC.save()
                managedObjectContext.performAndWait {
                    do {
                        try managedObjectContext.save()
                    } catch {
                        print("Could not synchonize data. \(error), \(error.localizedDescription)")
                    }
                }
            } catch {
                print("Could not synchonize data. \(error), \(error.localizedDescription)")
            }
        }
        
        func printThreadStats() {
            if Thread.isMainThread {
                print("on the main thread")
            } else {
                print("off the main thread")
            }
        }  
    }
    
    

    Apple 有一个很好的模板Using a Private Queue to Support Concurrency

    另一个有用的链接:Best practice: Core Data Concurrency

    真正棘手的事情是如何将它与您的视图或视图控制器连接起来,即真正的实现。请参阅下面的 ViewController 代码。

        // 1
        override func viewDidLoad() {
            super.viewDidLoad()
    
            // some layout code
    
            // execute on background thread
            DispatchQueue.global().async { [weak self] in
                self?.fetchAndReload()
            }
        }
        
        // 2
        private func fetchAndReload() {
            CoreDataManager.shared.fetchAllPlaylists(completion: { playlists in
                guard let playlists = playlists else { return }
                self.playlists = playlists
            })
            
            DispatchQueue.main.async {
                self.tableView.reloadData()
            }
        }
        
        // 3
        @objc func createNewPlaylist(_ sender: Any?) {
            let ac = UIAlertController(title: "Create New Playlist", message: "", preferredStyle: .alert)
            ac.addTextField { textField in
                textField.placeholder = "input your desired name"
            }
            ac.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
            
            ac.addAction(UIAlertAction(title: "Done", style: .default, handler: { [weak self] _ in
                guard let textField = ac.textFields?.first, let newName = textField.text, !newName.isEmpty else { return }
                // check duplicate
                if let playlists = self?.playlists {
                    if playlists.contains(where: { playlist in
                        playlist.name == newName
                    }) {
                        self?.duplicateNameAlert()
                        return
                    }
                }
    
                DispatchQueue.global().async { [weak self] in
                    CoreDataManager.shared.createPlaylist(name: newName)
                    self?.fetchAndReload()
                }
            }))
            
            present(ac, animated: true)
        }
    

    让我分解一下:

    1. 首先在 viewDidload 中,我们在后台线程上调用 fetchAndReload
    2. fetchAndReload函数中,它会带出所有播放列表(返回带有完成处理程序的数据)并在主线程上刷新表格。
    3. 我们在后台线程中调用createPlaylist(name: newName) 并再次在主线程中重新加载表。

    嗯,这是我第一次在CoreData中处理多线程,如有错误,请指出。好吧,就是这样!希望它可以帮助某人。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-09-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多