Our current SubscriptionStore is too tied to the main core data context. In this episode we'll split this behavior on to a new type that will manage persistence for us, as well as implement a solution to solve the problem of core data being initialized asynchronously. We want to delay our app's UI until we have a context we can use.
Episode Links Advanced Operations Series Creating an Import Operation To manage asynchronous operations for Core Data we'll use our BaseOperation which is largely based on the work in the Advanced Operations series. import Foundation class BaseOperation : Operation { override var isAsynchronous: Bool { return true } private var _executing = false { willSet { willChangeValue(forKey: "isExecuting") } didSet { didChangeValue(forKey: "isExecuting") } } override var isExecuting: Bool { return _executing } private var _finished = false { willSet { willChangeValue(forKey: "isFinished") } didSet { didChangeValue(forKey: "isFinished") } } override var isFinished: Bool { return _finished } override func start() { _executing = true execute() } func execute() { fatalError("You must override this") } func finish() { _executing = false _finished = true } } Now we'll create an operation to import all the episodes from a feed and save them to the database. To make sure that we use the context on the save thread it was created on, we'll wait for the execute method before creating our background context. import Foundation import CoreData class ImportEpisodesOperation : BaseOperation { private let podcastId: String private let feedLoader = PodcastFeedLoader() private var context: NSManagedObjectContext! private var subscriptionStore: SubscriptionStore! init(podcastId: String) { } override func execute() { } } Splitting the Responsibility of Subscription Store As the queries in the SubscriptionStore work on the main core data context, we will have to change this. Create a new type called PersistenceManager to manage creating the Core Data stack and maintaining a reference to the main context for us: import Foundation class PersistenceManager { static var shared = PersistenceManager() private let persistentContainer: NSPersistentContainer var mainContext: NSManagedObjectContext { return persistentContainer.viewContext } func newBackgroundContext() -> NSManagedObjectContext { return persistentContainer.newBackgroundContext() } private init() { persistentContainer = NSPersistentContainer(name: "Subscriptions") } func initializeModel() { persistentContainer.loadPersistentStores { (storeDescription, error) in if let error = error { fatalError("Core Data error: \(error.localizedDescription)") } else { print("Loaded Store: \(storeDescription.url?.absoluteString ?? "nil")") } } } } Our view context and background context will be only valid after the loading of the persistent store has completed. So far we have avoid this being an issue because we had no startup logic that used the context. However this is changing and we now need to consider that view controllers won't be able to immediately use the context. To avoid this, we'll add a condition and a completion block to defer any other logic to execute until the persistence store is loaded. import Foundation import CoreData class PersistenceManager { static var shared = PersistenceManager() private let persistentContainer: NSPersistentContainer private var isLoaded = false var mainContext: NSManagedObjectContext { precondition(isLoaded) return persistentContainer.viewContext } func newBackgroundContext() -> NSManagedObjectContext { precondition(isLoaded) return persistentContainer.newBackgroundContext() } private init() { persistentContainer = NSPersistentContainer(name: "Subscriptions") } func initializeModel(then completion: @escaping () -> Void) { persistentContainer.loadPersistentStores { (storeDescription, error) in if let error = error { fatalError("Core Data error: \(error.localizedDescription)") } else { self.isLoaded = true print("Loaded Store: \(storeDescription.url?.absoluteString ?? "nil")") completion() } } } } In the initializeModel method, we pass in a completion block so we can get notified when the stack is ready. Delaying the Loading of View Controllers We want to delay the loading of the main view controllers until the Core Data store is loaded. To do this we will disable launching with the storyboard and create a new window, which will show a blank screen. This blank screen will be displayed until Core Data is loaded. var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { window = UIWindow() Theme.apply(to: window!) window?.rootViewController = UIViewController() window?.makeKeyAndVisible() PersistenceManager.shared.initializeModel(then: { FeedImporter.shared.startListening() let storyboard = UIStoryboard(name: "Main", bundle: nil) self.window?.rootViewController = storyboard.instantiateInitialViewController() }) return true } Refactoring Subscription Store to use Persistence Manager Next, we'll refactor SubscriptionStore to use the PersistenceManager. Now we can pass the main or background context to the store instead of always using the main context. import Foundation import CoreData class PersistenceManager { static var shared = PersistenceManager() private let persistentContainer: NSPersistentContainer private var isLoaded = false var mainContext: NSManagedObjectContext { precondition(isLoaded) return persistentContainer.viewContext } func newBackgroundContext() -> NSManagedObjectContext { precondition(isLoaded) return persistentContainer.newBackgroundContext() } private init() { persistentContainer = NSPersistentContainer(name: "Subscriptions") } func initializeModel(then completion: @escaping () -> Void) { persistentContainer.loadPersistentStores { (storeDescription, error) in if let error = error { fatalError("Core Data error: \(error.localizedDescription)") } else { self.isLoaded = true print("Loaded Store: \(storeDescription.url?.absoluteString ?? "nil")") completion() } } } } Updating the View Controller Since we're no longer using a shared subscription store, we'll have to initialize this property in viewDidLoad. We'll pass in the main context for it to use similar to before. private var store: SubscriptionStore! private var podcast: Podcast? { didSet { podcastViewModel = podcast.flatMap { PodcastViewModel(podcast: $0, isSubscribed: store.isSubscribed(to: $0.id)) } } } override func viewDidLoad() { super.viewDidLoad() store = SubscriptionStore(context: PersistenceManager.shared.mainContext) tableView.backgroundColor = Theme.Colors.gray5 tableView.separatorColor = Theme.Colors.gray4 headerViewController = children.compactMap { $0 as? PodcastDetailHeaderViewController }.first headerViewController.subscribeButton.addTarget(self, action: #selector(subscribeTapped(_:)), for: .touchUpInside) loadPodcast() } @objc private func subscribeTapped(_ sender: SubscribeButton) { guard let podcast = podcast else { return } let isSubscribing = !sender.isSelected do { if isSubscribing { try store.subscribe(to: podcast) } else { try store.unsubscribe(from: podcast) } sender.isSelected.toggle() } catch { let action = isSubscribing ? "Subscribing to Podcast" : "Unsubscribing from Podcast" let alert = UIAlertController(title: "Error \(action)", message: error.localizedDescription, preferredStyle: .alert) alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) } } While loading the MyPodcast view, we'll refer to the main context of the persistence manager. private var store: SubscriptionStore! override func viewDidLoad() { super.viewDidLoad() store = SubscriptionStore(context: PersistenceManager.shared.mainContext) loadSubscriptions() subscriptionChangedObserver = NotificationCenter.default .addObserver(SubscriptionsChanged.self, sender: nil, queue: .main, using: { change in self.loadSubscriptions() }) } private func loadSubscriptions() { do { subscriptions = try store.fetchSubscriptions() tableView.reloadData() } catch { print("ERROR: ", error.localizedDescription) } }