iOS 10 ships on September 13th. Apple has changed their generated code for Core Data. It doesn’t compile if you set your deployment target to iOS 9.3. What’s a developer to do?
For a very long time now, Apple has be proud, and deservedly so, that their users are overwhelmingly keeping up-to-date with the latest releases of their operating systems. As such, Apple’s messaging to developers is to support the current and previous OS version. Most of the time, this isn’t a big deal. Sometimes, it is. With the latest changes related to Core Data, it’s a problem.
If you create a new Xcode project that will being using Core Data, you get some useful code bits in the AppDelegate. In Xcode 8, you get the following (comments removed for brevity):
lazy var persistentContainer: NSPersistentContainer = { let container = NSPersistentContainer(name: "coreme") container.loadPersistentStores(completionHandler: { (storeDescription, error) in if let error = error as NSError? { fatalError("Unresolved error \(error), \(error.userInfo)") } }) return container }() func saveContext () { let context = persistentContainer.viewContext if context.hasChanges { do { try context.save() } catch { let nserror = error as NSError fatalError("Unresolved error \(nserror), \(nserror.userInfo)") } } }
This works great if you’re only going to build for iOS 10, but if you try to set your deployment target to iOS 9.3 as recommended, you’ll get the following error:
AppDelegate.swift:49:35: 'NSPersistentContainer' is only available on iOS 10.0 or newer
Well, that’s not good.
As users of Core Data know, operations using it use a database context. You can see it being used in the saveContext function above. Here we see that the context is a member of the persistentContainer. Since this doesn’t exist in iOS 9.3, we’ll have to get it some other way.
Let’s look at what used to be supplied with Xcode 7 (udpated to Swift 3):
lazy var applicationDocumentsDirectory: URL = { let urls = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) return urls[urls.count-1] }() lazy var managedObjectModel: NSManagedObjectModel = { let modelURL = Bundle.main.url(forResource: "bfoo", withExtension: "momd")! return NSManagedObjectModel(contentsOf: modelURL)! }() lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = { let coordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel) let url = self.applicationDocumentsDirectory.appendingPathComponent("SingleViewCoreData.sqlite") do { try coordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: url, options: nil) } catch { let dict : [String : Any] = [NSLocalizedDescriptionKey : "Failed to initialize the application's saved data" as NSString, NSLocalizedFailureReasonErrorKey : "There was an error creating or loading the application's saved data." as NSString, NSUnderlyingErrorKey : error as NSError] let wrappedError = NSError(domain: "YOUR_ERROR_DOMAIN", code: 9999, userInfo: dict) print("Unresolved error \(wrappedError), \(wrappedError.userInfo)") abort() } return coordinator }() lazy var managedObjectContext: NSManagedObjectContext = { let coordinator = self.persistentStoreCoordinator var managedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) managedObjectContext.persistentStoreCoordinator = coordinator return managedObjectContext }()
I’ve left out the saveContext function as it’s only distinction is where it gets the context from. Above, we see that the context is a separate variable.
Now without a doubt, the iOS 10 code is much simpler, but, if we want to be able to do the right thing and support both current and last version, we need a bit of a mash up. It’s a bit tedious, but you only have to build it once (hint, hint Apple). Here’s what I’ve come up with.
// MARK: - utility routines lazy var applicationDocumentsDirectory: URL = { let urls = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) return urls[urls.count-1] }() // MARK: - Core Data stack (generic) lazy var managedObjectModel: NSManagedObjectModel = { let modelURL = Bundle.main.url(forResource: modelName, withExtension: modelExtension)! return NSManagedObjectModel(contentsOf: modelURL)! }() lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = { let coordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel) let urls = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) let url = self.applicationDocumentsDirectory.appendingPathComponent(modelName).appendingPathExtension(sqliteExtension) do { try coordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: url, options: nil) } catch { let dict : [String : Any] = [NSLocalizedDescriptionKey : "Failed to initialize the application's saved data" as NSString, NSLocalizedFailureReasonErrorKey : "There was an error creating or loading the application's saved data." as NSString, NSUnderlyingErrorKey : error as NSError] let wrappedError = NSError(domain: "YOUR_ERROR_DOMAIN", code: 9999, userInfo: dict) fatalError("Unresolved error \(wrappedError), \(wrappedError.userInfo)") } return coordinator }() // MARK: - Core Data stack (iOS 9) @available(iOS 9.0, *) lazy var managedObjectContext: NSManagedObjectContext = { var managedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) managedObjectContext.persistentStoreCoordinator = self.persistentStoreCoordinator return managedObjectContext }() // MARK: - Core Data stack (iOS 10) @available(iOS 10.0, *) lazy var persistentContainer: NSPersistentContainer = { let container = NSPersistentContainer(name: creditsModelName) container.loadPersistentStores(completionHandler: { (storeDescription, error) in if let error = error as NSError? { fatalError("Unresolved error \(error), \(error.userInfo)") } } ) return container }() // MARK: - Core Data context lazy var databaseContext : NSManagedObjectContext = { if #available(iOS 10.0, *) { return self.persistentContainer.viewContext } else { return self.managedObjectContext } }() // MARK: - Core Data save func saveContext () { do { if databaseContext.hasChanges { try databaseContext.save() } } catch { let nserror = error as NSError fatalError("Unresolved error \(nserror), \(nserror.userInfo)") } }
I’ve introduced databaseContext as the context variable to be used throughout. I think it’s more clear and now saveContext doesn’t have to care which OS you’re targeting. With this change in place, you can now safely target 9.3 and still build for 10.0.
Leave a Reply