我正在使用Alamofire使用Swift向端点提交请求。我使用可编码协议解析从响应接收到的JSON对象,然后尝试使用托管对象子类将这些对象插入核心数据。但是,当我这样做时,我一直收到一个错误,说我的父托管对象上下文(MOC)为零。这对我来说没有意义,因为我通过AppDelegate的依赖注入来设置MOC,并通过在viewDidLoad()方法中将其值输出到控制台来确认它有一个值。
这是我的相关代码:
我在这里设置了主控室:

class ViewController: UIViewController {

var managedObjectContext: NSManagedObjectContext! {
    didSet {
        print("moc set")
    }
}


override func viewDidLoad() {
    super.viewDidLoad()
    print(managedObjectContext)
}

///
func registerUser(userID: String, password: String) {

    let parameters: [String: Any] = ["email": userID, "password": password, "domain_id": 1]
    let headers: HTTPHeaders = ["Accept": "application/json"]

    Alamofire.request(registerURL, method: .patch, parameters: parameters, encoding: JSONEncoding.default, headers: headers).responseJSON { response in

        switch response.result {
        case .success:

            if let value = response.result.value {
                print(value)
                let jsonDecoder = JSONDecoder()

                do {
                    let jsonData = try jsonDecoder.decode(JSONData.self, from: response.data!)
                    print(jsonData.data.userName)
                    print(jsonData.data.identifier)
                    print(self.managedObjectContext)

                    let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
                    privateContext.parent = self.managedObjectContext

                    let user = UserLogin(context: privateContext)
                    user.userName = jsonData.data.userName
                    user.domainID = Int16(jsonData.data.identifier)
                    user.password = "blah"

                    do {
                        try privateContext.save()
                        try privateContext.parent?.save()
                    } catch let saveErr {
                        print("Failed to save user", saveErr)
                    }

                } catch let jsonDecodeErr{
                    print("Failed to decode", jsonDecodeErr)
                }

            }
        case .failure(let error):
            print(error)
        }
    }
}

我收到的具体错误消息是:
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Parent NSManagedObjectContext must not be nil.'

我意识到Alamofire是在后台线程上下载数据的,这就是我使用子上下文的原因,但我不确定父上下文为nil的原因。
以下是托管对象上下文的设置代码:
class AppDelegate: UIResponder, UIApplicationDelegate {

var persistentContainer: NSPersistentContainer!
var window: UIWindow?


func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    // Override point for customization after application launch.

    createContainer { container in

        self.persistentContainer = container
        let storyboard = self.window?.rootViewController?.storyboard
        guard let vc = storyboard?.instantiateViewController(withIdentifier: "RootViewController") as? ViewController else { fatalError("Cannot instantiate root view controller")}
        vc.managedObjectContext = container.viewContext
        self.window?.rootViewController = vc

    }

    return true
}

func createContainer(completion: @escaping(NSPersistentContainer) -> ()) {

    let container = NSPersistentContainer(name: "Test")
    container.loadPersistentStores { _, error in

        guard error == nil else { fatalError("Failed to load store: \(error!)") }
        DispatchQueue.main.async { completion(container) }

    }
}

有人知道我做错了什么吗?

最佳答案

我看不出任何立即“出错”的地方,所以让我们调试一下。
在警卫后面的applicationDidFinish...中设置一个断点。
在创建privateContext时设置断点。
哪个先开火?
registerUser函数在哪里?在视图控制器中?我希望不会:)
在我的guard语句之后的断点首先触发。是的,我的registerUser函数确实在ViewController中。
将网络代码放到视图控制器中是一种代码味道。视图控制器有一个作业,管理其视图。数据收集属于持久性控制器;例如,扩展NSPersistentContainer并将数据收集代码放在那里。
不过,这不是问题所在,只是代码的味道。
下一个测试。
持久化容器和/或viewContext是否被传递到视图控制器并保留?
你的视图控制器在街区起火前被摧毁了吗?
为了测试这一点,我将在Alamofire.request之前放置一个断言,如果上下文是nil,则会崩溃:

NSAssert(self.managedObjectContext != nil, @"Main context is nil in the view controller");

我也会把同一行代码放在前面:
privateContext.parent = self.managedObjectContext

再跑一次。会发生什么?
我按照您描述的方式运行测试,得到错误:线程1:断言失败:视图控制器中的主上下文为nil
哪个断言崩溃了?(可能应该稍微更改一下文本…)
如果是第一个,那么您的视图控制器不会接收到viewContext
如果是第二个,那么viewContext将在块执行之前返回到nil
相应地改变你的假设。
在这里发现了一些相关的东西:如果我放置一个按钮来调用registerUser()函数,而不是直接从viewDidLoad()方法调用它,就不会发生崩溃,代码运行良好,MOC有一个值
这就导致了我的理论,你的registerUser()在你的viewDidLoad()之前被调用。你可以通过在两者中放一个断点来测试,看哪一个先开火。如果您的registerUser()首先触发,请查看堆栈并查看调用它的是什么。
如果它在viewDidLoad()之后触发,那么在context属性上设置一个断点,看看是什么将它设置回nil
所以,如果我删除了这一行,如何通过依赖注入在RootViewController上设置MOC属性?
前面的那条线就是这里的线索。
let storyboard = self.window?.rootViewController?.storyboard

在这里,您将从storyboard中获取对rootViewController的引用,该引用已经实例化并与应用程序的window关联。
因此,您可以将逻辑更改为:
(self.window?.rootViewController as? ViewController).managedObjectContext = container.viewContext

尽管我会把它清理干净,并在它周围放置一些nil逻辑:)
我意识到的问题是,RootViewController中的MOC在MOC从闭包返回并在AppDelegate中设置之前被使用。我在这里做什么?
这是一个常见的同步(UI)与异步(持久性)问题。理想情况下,您的UI应该等到持久性加载。如果您加载持久性存储,然后在存储加载后完成UI,它将解决此问题。
如果没有迁移,我们通常在这里谈论ms,而不是秒。
但是。。。无论是毫秒还是秒,您都希望相同的代码处理UI加载。如何解决这个问题取决于你自己(设计决策)。一个例子是继续加载视图,直到持久层准备好,然后转换过来。
如果您这样做了,那么您可以在迁移发生时微妙地更改加载视图,以告知用户为什么要花这么长时间。

10-08 05:22