4

I’m trying to get my head around NSManagedObjectContext for Core Data. Xcode 10.1 provides a fair amount of boilerplate if the Core Data checkbox is selected when creating a new project. But I find it a bit confusing wrt how the current context is set for each view controller. I think I have a better way and am looking for advice to confirm, or to put me back on the right track.

For example, in the boilerplate AppDelegate code, didFinishLaunchingWithOptions provides the context to the MasterViewController like this:

let masterNavigationController = splitViewController.viewControllers[0] as! UINavigationController
let controller = masterNavigationController.topViewController as! MasterViewController
controller.managedObjectContext = self.persistentContainer.viewContex

In the MasterViewContoller, the first use of context picks it up from the fetchedResultsController AND there is code to save the context provided, even though the AppDelegate already has a saveContext() function available to do the same thing:

@objc
func insertNewObject(_ sender: Any) {
    let context = self.fetchedResultsController.managedObjectContext
    let newEvent = Event(context: context)

    // If appropriate, configure the new managed object.
    newEvent.timestamp = Date()

    // Save the context.
    do {
        try context.save()
    } catch {
        // Replace this implementation with code to handle the error appropriately.
        // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
        let nserror = error as NSError
        fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
    }
}

In my app with multiple view controllers I have made mistakes trying to re-declare or hand off the context in each one where it’s needed, so had to contend with errors that were caused by inadvertently having more than one context flying around.

So my question is this: Am I making a mistake, or is there some downside to the following approach:

1) Make the AppDelegate a singleton:

class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDelegate {

var window: UIWindow?

static let shared = AppDelegate()
…

2) In each class where it’s needed, always define the context (I’m assuming I only need one) like this:

let context = AppDelegate.shared.persistentContainer.viewContext

3) Whenever the context needs to be saved, do it like this:

AppDelegate.shared.saveContext()

That seems much simpler, clearer and less prone to errors, and seems to work in my implementation. Are there problems with this that I'm not seeing?

tkhelm
  • 345
  • 3
  • 14

2 Answers2

4

To be honest Apple examples / templates always was bad example for beginners, because they show only one thing and "hack" on rest (force unwrapping everything for example). And beginners tend to just copy this approach.

Disclaimer: I talking about middle-big size applications. You always can broke this rules and recommendations in small apps, cause not using them can be easier and lead to simpler application.

Make the AppDelegate a singleton:

In 99% you should not instantiate AppDelegate by self. It handled for you by UIApplication / @UIApplicationMain annotation.

AppDelegate already singleton, since each application has exactly one delegate for whole lifetime. You can access it by UIApplication.shared.delegate as? AppDelegate.

But you shouldn't. AppDelegate play specific role in each app by providing entry point for communication between system and your code and you shouldn't add additional roles to it (as handle database). Accessing it somewhere in codebase in most case signs of code smell and bad architecture.

Separating CoreData stack

DataBase access is one of few examples of good use of Singleton pattern. But instead of using AppDelegate you should make separate service that will be responsible only for handling communication with coredata (such as creating and handling stack, sending queries and etc).

So CoreDataService is way to go.

Accessing core data

Using singletons doesn't mean you can just access it anywhere by typing Singleton.shared. This will highly decrease testability of your components and make them highly coupled to singletons.

Instead you should read about Dependency injection principle and inject your singletons. E.g:

class MyViewController: UIViewController {
    let dataBaseManager: CoreDataService
    init(with dataBaseManager: CoreDataService) {
        self.dataBaseManager = dataBaseManager
        super.init(nibName: nil, bundle: nil)
    }
}

Ideally you should go even further to SOLID and provide to controller only what it really needs:

protocol EventsProvider {
    func getEvents(with callback: [Event] -> Void)
}

extension CoreDataService: EventsProvider {
    func getEvents(with callback: [Event] -> Void) { 
        // your core data query here
    }
}

class MyViewController: UIViewController {
    let eventsProvider: EventsProvider
    init(with eventsProvider: EventsProvider) {
        self.eventsProvider = eventsProvider
        super.init(nibName: nil, bundle: nil)
    }
}

let vc = MyViewController(with: CoreDataService.shared)

Multiple contexts

Having multiple NSManagedObjectContext can be handy and improve performance, but only when you know how to work with them.
It's more advance topic, so you can ignore it for now.
You can read about it in Core Data Programming Guide

ManWithBear
  • 2,787
  • 15
  • 27
0

An interesting experiment is to utilise the responder chain and move the insertNewObject method to the app delegate and change it to call [self saveContext]. Then have the add button send the action to nil instead of self. Or in the storyboard drag on a bar button item and drag from its action to the first responder icon (it’s at the top of the view controller and also in the left side bar) and select insertNewObject and try it out!

malhal
  • 26,330
  • 7
  • 115
  • 133