iosswiftcore-dataalamofire

Managed Object Context is nil for some reason in iOS


I'm using Alamofire to submit a request to an endpoint using Swift. I parse the JSON objects that I receive from the response using the Codable protocol, and then try to insert the objects into Core Data using Managed Object Subclasses.

However, when I do this, I keep receiving an error saying that my parent Managed Object Context (MOC) is nil. This doesn't make sense to me because I set the MOC via Dependency Injection from the AppDelegate, and confirm that it has a value by printing out it's value to the console in the viewDidLoad() method.

Here is my relevant code:

I set my MOC here:

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)
        }
    }
}

The specific error message I'm getting is:

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Parent NSManagedObjectContext must not be nil.'

I realize that Alamofire is download the data on a background thread, which is why I use a child context, but I'm not sure why the parent is nil.

Here is the setup code for my Managed Object Context:

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) }
        
    }
}

What am I doing wrong?


Solution

  • I don't see anything immediately "wrong" so lets debug this a bit.

    1. Put a breakpoint in applicationDidFinish... right after the guard.
    2. Put a breakpoint at the creation of the privateContext.

    Which fires first?

    Where is the registerUser function? In a view controller? I hope not :)

    the breakpoint right after my guard statement fires first. And yes, my registerUser function is indeed inside a ViewController.

    Putting network code in view controllers is a code smell. View Controllers have one job, manage their views. Data gathering belongs in a persistence controller; for example, extending your NSPersistentContainer and putting data collection code there.

    However, that is not the issue here, just a code smell.

    Next test.

    Is your persistent container and/or viewContext being passed into your view controller and bring retained?

    Is your view controller being destroyed before the block fires?

    To test this, I would put an assertion before Alamofire.request and crash if the context is nil:

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

    I would also put that same line of code just before:

    privateContext.parent = self.managedObjectContext
    

    Run again. What happens?

    I ran the test as you described, and I get the error: Thread 1: Assertion failed: Main context is nil in the view controller

    Which assertion crashed? (probably should change the text a bit...)

    If it is the first one then your view controller is not receiving the viewContext.

    If it is the second one then the viewContext is going back to nil before the block executes.

    Change your assumptions accordingly.

    discovered something that is relevant here: If I place a button to call the registerUser() function at the user's discretion instead of calling it directly from the viewDidLoad() method, there is no crash, the code runs fine, and the MOC has a value

    That leads me down the theory that your registerUser() was being called before your viewDidLoad(). You can test that by putting a break point in both and see which one fires first. If your registerUser() fires first, look at the stack and see what is calling it.

    If it fires after your viewDidLoad() then put a breakpoint on the context property and see what is setting it back to nil.

    So if I remove that line, how do I set the MOC property on my RootViewController via Dependency Injection?

    The line right before it is the clue here.

    let storyboard = self.window?.rootViewController?.storyboard
    

    Here you are getting a reference to the storyboard from the rootViewController which is already instantiated and associated with the window of your application.

    Therefore you could change the logic to:

    (self.window?.rootViewController as? ViewController).managedObjectContext = container.viewContext
    

    Although I would clean that up and put some nil logic around it :)

    The problem I realize is that the MOC in the RootViewController is being used BEFORE the MOC is returned from the closure, and set in the AppDelegate. What do I do here?

    This is a common synchronous (UI) vs. asynchronous (persistence) issue. Ideally your UI should wait until the persistence loads. If you load the persistent stores and then finish the UI after the stores have loaded it will resolve this issue.

    Without a migration we are generally talking ms here rather than seconds.

    BUT ... You want the same code to handle the UI loading whether it is milliseconds or seconds. How you solve that is up to you (design decision). One example is to continue the loading view until the persistence layer is ready and then transition over.

    If you did that you could then subtly change the loading view if a migration is happening to inform the user as to why things are taking so long.