
This video is only available to subscribers. Start a subscription today to get access to this and 472 other videos.
Refactor Core Data Context Handling
This episode is part of a series: Making a Podcast App From Scratch.
Episode Links
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)
}
}