Core Data を既に使用しているプロジェクトがあります。iPad のサポートを追加しましたが、データを同期するには iCloud と Core Data を使用する必要があります。
Ensembles に出会いました。プロジェクトに追加するのは簡単で堅牢なフレームワークのようです。ここにあります: https://github.com/drewmccormack/ensembles
ただし、Ensembles プロジェクトには Swift のサンプル プロジェクトがないため、自分でやろうとしました。ここに私が取ったステップがあります、
ステップ1
アンサンブルを iOS プロジェクトに手動で追加します。
ステップ2
既存の永続ストア .sql ファイルを使用して、新しい CoreDataStack を作成します。
import UIKit
import CoreData
class CoreDataStack: NSObject, CDEPersistentStoreEnsembleDelegate {
static let defaultStack = CoreDataStack()
var ensemble : CDEPersistentStoreEnsemble? = nil
var cloudFileSystem : CDEICloudFileSystem? = nil
// MARK: - Core Data stack
lazy var storeName : String = {
return NSBundle.mainBundle().objectForInfoDictionaryKey(kCFBundleNameKey as String) as! String
}()
lazy var sqlName : String = {
return "SingleViewCoreData.sqlite"
}()
lazy var icloudStoreName : String = {
return self.storeName + "CloudStore"
}()
lazy var storeDescription : String = {
return "Core data stack of " + self.storeName
}()
lazy var iCloudAppID : String = {
return "iCloud." + NSBundle.mainBundle().bundleIdentifier!
}()
lazy var modelURL : NSURL = {
return NSBundle.mainBundle().URLForResource(self.storeName, withExtension: "momd")!
}()
lazy var storeDirectoryURL : NSURL = {
var directoryURL : NSURL? = nil
do {
try directoryURL = NSFileManager.defaultManager().URLForDirectory(NSSearchPathDirectory.ApplicationSupportDirectory, inDomain: .UserDomainMask, appropriateForURL: nil, create: true)
directoryURL = directoryURL!.URLByAppendingPathComponent(NSBundle.mainBundle().bundleIdentifier!, isDirectory: true)
} catch {
NSLog("Unresolved error: Application's document directory is unreachable")
abort()
}
return directoryURL!
}()
lazy var storeURL : NSURL = {
return self.storeDirectoryURL.URLByAppendingPathComponent(self.sqlName)
// return self.applicationDocumentsDirectory.URLByAppendingPathComponent(self.sqlName)
}()
lazy var applicationDocumentsDirectory: NSURL = {
// The directory the application uses to store the Core Data store file. This code uses a directory named "com.dprados.CoreDataSpike" in the application's documents Application Support directory.
let urls = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)
return urls[urls.count-1]
}()
lazy var managedObjectModel: NSManagedObjectModel = {
// The managed object model for the application. This property is not optional. It is a fatal error for the application not to be able to find and load its model.
let modelURL = NSBundle.mainBundle().URLForResource(self.storeName, withExtension: "momd")
return NSManagedObjectModel(contentsOfURL: modelURL!)!
}()
lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator? = {
// The persistent store coordinator for the application. This implementation creates and returns a coordinator, having added the store for the application to it. This property is optional since there are legitimate error conditions that could cause the creation of the store to fail.
// Create the coordinator and store
let coordinator : NSPersistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)
var options = [NSObject: AnyObject]()
options[NSMigratePersistentStoresAutomaticallyOption] = NSNumber(bool: true)
options[NSInferMappingModelAutomaticallyOption] = NSNumber(bool: true)
do {
try NSFileManager.defaultManager().createDirectoryAtURL(self.storeDirectoryURL, withIntermediateDirectories: true, attributes: nil)
} catch {
NSLog("Unresolved error: local database storage position is unavailable.")
abort()
}
// Create the coordinator and store
let url = self.applicationDocumentsDirectory.URLByAppendingPathComponent("SingleViewCoreData.sqlite")
var failureReason = "There was an error creating or loading the application's saved data."
do {
try coordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: url, options: nil)
} catch {
// Report any error we got.
var dict = [String: AnyObject]()
dict[NSLocalizedDescriptionKey] = "Failed to initialize the application's saved data"
dict[NSLocalizedFailureReasonErrorKey] = failureReason
dict[NSUnderlyingErrorKey] = error as! NSError
let wrappedError = NSError(domain: "YOUR_ERROR_DOMAIN", code: 9999, userInfo: dict)
// Replace this with code to handle the error appropriately.
// abort() 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.
NSLog("Unresolved error \(wrappedError), \(wrappedError.userInfo)")
abort()
}
return coordinator
}()
lazy var managedObjectContext: NSManagedObjectContext = {
// Returns the managed object context for the application (which is already bound to the persistent store coordinator for the application.) This property is optional since there are legitimate error conditions that could cause the creation of the context to fail.
let coordinator = self.persistentStoreCoordinator
var managedObjectContext = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType)
managedObjectContext.persistentStoreCoordinator = coordinator
managedObjectContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
return managedObjectContext
}()
// MARK: - Core Data Saving support
func saveContext () {
if managedObjectContext.hasChanges {
do {
try managedObjectContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// abort() 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
NSLog("Unresolved error \(nserror), \(nserror.userInfo)")
abort()
}
}
}
static func save() {
CoreDataStack.defaultStack.saveContext()
}
func enableEnsemble() {
CoreDataStack.defaultStack.cloudFileSystem = CDEICloudFileSystem(ubiquityContainerIdentifier: nil)
CoreDataStack.defaultStack.ensemble = CDEPersistentStoreEnsemble(ensembleIdentifier: self.storeName, persistentStoreURL: self.storeURL, managedObjectModelURL: self.modelURL, cloudFileSystem: CoreDataStack.defaultStack.cloudFileSystem)
CoreDataStack.defaultStack.ensemble!.delegate = CoreDataStack.defaultStack
}
func persistentStoreEnsemble(ensemble: CDEPersistentStoreEnsemble!, didSaveMergeChangesWithNotification notification: NSNotification!) {
CoreDataStack.defaultStack.managedObjectContext.performBlockAndWait({ () -> Void in
CoreDataStack.defaultStack.managedObjectContext.mergeChangesFromContextDidSaveNotification(notification)
})
if notification != nil {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(0.02 * Double(NSEC_PER_MSEC))), dispatch_get_main_queue(), {
NSLog("Database was updated from iCloud")
CoreDataStack.defaultStack.saveContext()
NSNotificationCenter.defaultCenter().postNotificationName("DB_UPDATED", object: nil)
})
}
}
func persistentStoreEnsemble(ensemble: CDEPersistentStoreEnsemble!, globalIdentifiersForManagedObjects objects: [AnyObject]!) -> [AnyObject]! {
NSLog("%@", (objects as NSArray).valueForKeyPath("uniqueIdentifier") as! [AnyObject])
return (objects as NSArray).valueForKeyPath("uniqueIdentifier") as! [AnyObject]
}
func syncWithCompletion(completion: (() -> Void)!) {
if CoreDataStack.defaultStack.ensemble!.leeched {
CoreDataStack.defaultStack.ensemble!.mergeWithCompletion({ (error:NSError?) -> Void in
if error != nil && error!.code != 103 {
NSLog("Error in merge: %@", error!)
} else if error != nil && error!.code == 103 {
self.performSelector("syncWithCompletion:", withObject: nil, afterDelay: 1.0)
} else {
if completion != nil {
completion()
}
}
})
} else {
CoreDataStack.defaultStack.ensemble!.leechPersistentStoreWithCompletion({ (error:NSError?) -> Void in
if error != nil && error!.code != 103 {
NSLog("Error in leech: %@", error!)
} else if error != nil && error!.code == 103 {
self.performSelector("syncWithCompletion:", withObject: nil, afterDelay: 1.0)
} else {
self.performSelector("syncWithCompletion:", withObject: nil, afterDelay: 1.0)
if completion != nil {
completion()
}
}
})
}
}
}
ステップ 3
App Delegate を更新して同期し、通知を追加する
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
let _ : CoreDataStack = CoreDataStack.defaultStack
// Value.ValueTypeInManagedObjectContext(CoreDataStack.defaultStack.managedObjectContext)
CoreDataStack.defaultStack.saveContext()
CoreDataStack.defaultStack.enableEnsemble()
// Listen for local saves, and trigger merges
NSNotificationCenter.defaultCenter().addObserver(self, selector: "localSaveOccured:", name: CDEMonitoredManagedObjectContextDidSaveNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: "cloudDataDidDownload:", name:CDEICloudFileSystemDidDownloadFilesNotification, object:nil)
CoreDataStack.defaultStack.syncWithCompletion(nil);
return true
}
func applicationDidEnterBackground(application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
let identifier : UIBackgroundTaskIdentifier = UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler(nil)
CoreDataStack.defaultStack.saveContext()
CoreDataStack.defaultStack.syncWithCompletion( { () -> Void in
UIApplication.sharedApplication().endBackgroundTask(identifier)
})
}
func applicationWillResignActive(application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
}
func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject]) {
NSLog("Received a remove notification")
}
func applicationWillEnterForeground(application: UIApplication) {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
CoreDataStack.defaultStack.syncWithCompletion(nil)
}
func applicationDidBecomeActive(application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
CoreDataStack.defaultStack.syncWithCompletion(nil)
}
func applicationWillTerminate(application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
// Saves changes in the application's managed object context before the application terminates.
CoreDataStack.defaultStack.saveContext()
}
func localSaveOccured(notif: NSNotification) {
NSLog("Local save occured")
CoreDataStack.defaultStack.syncWithCompletion(nil)
}
func cloudDataDidDownload(notif: NSNotification) {
NSLog("Cloud data did download")
CoreDataStack.defaultStack.syncWithCompletion(nil)
}
ステップ 4
プロジェクトに通知を追加して UI を更新する
override func viewWillAppear(animated: Bool) {
NSNotificationCenter.defaultCenter().addObserver(self, selector: "cloudDataDidDownload:", name:"DB_UPDATED", object:nil)
//cloudDataDidDownload refetches the entities and reload the table
}
ステップ 5
魔法が起こるのを見てください。残念ながら、魔法の ATM はありません。新しい CoreDataStack は正常に動作し、永続ストアからデータを保存および取得できます。
同じ iCloud アカウントにログインしている 2 つのデバイスがあり、どちらのデータも他のデバイスと共有されていません。
アプリを削除して再インストールすると、データは iCloud から取得されず、永続ストアに保存されます。
NSLog
「時々」データを保存したり、アプリをロードしたりすると、次のようになります。
2016-04-06 13:17:37.101 APPNAME[435:152241] Cloud data did download
これは、次の appDelegate 通知関数の結果です
func cloudDataDidDownload(notif: NSNotification) {
NSLog("Cloud data did download")
CoreDataStack.defaultStack.syncWithCompletion(nil)
}
変更がマージされると、CoreDataStack のこの関数から通知が送信されます。
func persistentStoreEnsemble(ensemble: CDEPersistentStoreEnsemble!, didSaveMergeChangesWithNotification notification: NSNotification!) {
CoreDataStack.defaultStack.managedObjectContext.performBlockAndWait({ () -> Void in
CoreDataStack.defaultStack.managedObjectContext.mergeChangesFromContextDidSaveNotification(notification)
})
if notification != nil {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(0.02 * Double(NSEC_PER_MSEC))), dispatch_get_main_queue(), {
NSLog("Database was updated from iCloud")
CoreDataStack.defaultStack.saveContext()
NSNotificationCenter.defaultCenter().postNotificationName("DB_UPDATED", object: nil)
})
}
}
したがって、すべてが正常に機能するはずです。エラーは発生しませんが、データが同期されていません。問題が iCloud へのデータのバックアップなのか、それとも iCloud からの取得と永続ストアとのマージなのかはわかりません。私が言えることは、同じ iCloud アカウントを使用しているデバイス間でデータが共有されておらず、アプリを再インストールするときにアプリが実際に iCloud からデータを復元していないということだけです。