I would like to use Ensembles Framework to synch my core data with Swift.
Ensembles Framework
But I have some difficulties..
I try to do it like that: (Similar way to the example on github)
I use a button to launch the tasks:
class ReglagesVC: UIViewController,UITableViewDataSource,UITableViewDelegate,UITextFieldDelegate,CDEPersistentStoreEnsembleDelegate {
#IBAction func IcloudSynch(_ sender: UIButton) {
CDESetCurrentLoggingLevel(CDELoggingLevel.verbose.rawValue)
// Setup Core Data Stack
self.setupCoreData()
// Setup Ensemble
let modelURL = Bundle.main.url(forResource: "Mes_Vide_os", withExtension: "momd")
cloudFileSystem = CDEICloudFileSystem(ubiquityContainerIdentifier: nil)
ensemble = CDEPersistentStoreEnsemble(ensembleIdentifier: "Mes_Vide_os", persistentStore: storeURL, managedObjectModelURL: modelURL!, cloudFileSystem: cloudFileSystem)
ensemble.delegate = self
// Listen for local saves, and trigger merges
NotificationCenter.default.addObserver(self, selector:#selector(localSaveOccurred(_:)), name:NSNotification.Name.CDEMonitoredManagedObjectContextDidSave, object:nil)
NotificationCenter.default.addObserver(self, selector:#selector(cloudDataDidDownload(_:)), name:NSNotification.Name.CDEICloudFileSystemDidDownloadFiles, object:nil)
// Sync
self.sync(nil)
}
//ENSEMBLES
// MARK: Notification Handlers
func localSaveOccurred(_ notif: Notification) {
self.sync(nil)
}
func cloudDataDidDownload(_ notif: Notification) {
self.sync(nil)
}
let appDelegate = UIApplication.shared.delegate as! AppDelegate
var storeDirectoryURL: URL {
return try! FileManager.default.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
}
var storeURL: URL {
return self.storeDirectoryURL.appendingPathComponent("store.sqlite")
}
var managedObjectContext : NSManagedObjectContext!
func setupCoreData() {
let modelURL = Bundle.main.url(forResource: "Mes_Vide_os", withExtension: "momd")//"momd"
let model = NSManagedObjectModel(contentsOf: modelURL!)
try! FileManager.default.createDirectory(at: self.storeDirectoryURL, withIntermediateDirectories: true, attributes: nil)
let coordinator = NSPersistentStoreCoordinator(managedObjectModel: model!)
let options = [NSMigratePersistentStoresAutomaticallyOption: true, NSInferMappingModelAutomaticallyOption: true]
try! coordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: self.storeURL, options: options)
managedObjectContext = appDelegate.persistentContainer.viewContext
managedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
managedObjectContext.persistentStoreCoordinator = coordinator
managedObjectContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
}
// MARK: Ensembles
var cloudFileSystem: CDECloudFileSystem!
var ensemble: CDEPersistentStoreEnsemble!
func sync(_ completion: (() -> Void)?) {
//let viewController = self.window?.rootViewController as! ReglagesVC
//self.activityIndicator?.startAnimating()
if !ensemble.isLeeched {
ensemble.leechPersistentStore {
error in
print("LEECH FINI___________________________")
completion?()
}
}
else {
ensemble.merge {
error in
print("MERGE FINI___________________________")
completion?()
}
}
}
func persistentStoreEnsemble(_ ensemble: CDEPersistentStoreEnsemble, didSaveMergeChangesWith notification: Notification) {
managedObjectContext.performAndWait {
self.managedObjectContext.mergeChanges(fromContextDidSave: notification)
}
}
func persistentStoreEnsemble(_ ensemble: CDEPersistentStoreEnsemble!, globalIdentifiersForManagedObjects objects: [Any]!) -> [Any]! {
let Films = objects as! [BaseFilms]
print("films map")
return Films.map { $0.id }
}
}
And when I login my cloudkit dashboard I see that a container with has been created but there isn't any recors in the datas folder.
And nothing is synchonised between my devices.
Can you tell me where I am wrong??
Thank you.
By putting the setup code in an action, my guess is you are creating a new Ensemble (and Core Data stack) every time you press the button. You should setup the stack and ensemble once, perhaps on launch in viewDidLoad, and keep it in a property.
Note that the first time you call sync, it will "leech". This involves importing your local data, but it does not upload anything. The second time you call sync, it will download data from the cloud and upload. So you need to call sync twice — with the same ensemble object — before you will see any data in the CloudKit web interface.
Note also that you can only see data in CloudKit from the logged in user. So you still won't see anything unless you login there with the account you are testing with.
To solve my issues:
Put the code in the correct place (in the AppDelegate).
Install the latest update of Ensembles (1.7.1 instead of 1.7).
And that's all! Thank you Drew.
Just one thing still strange: When I add an object in one device it is synchronised on the other but when I delete an object it's not deleted on the other device and created again on the first device.
Related
I have a NSManagedObject called Event that is shared between the host app and today extension. (In Target Membership, both the main app and the widget are checked).
The host app and widget have the same App Group identifier and both share Data Model(In Target Membership, both the main app and the widget are checked).
When I launch(run) the widget in Xcode, it shows all of the app events (Event) that are already saved in the host app. However, when I add a new event, it appears in the host app but NOT in today-widget. If I relaunch the widget, all the events are shown including the last event that previously was not.
This is the method that fetches events. It is defined in TodayViewController of the widget.
private func fetchEvents(date: Date) {
let predicates = NSCompoundPredicate(andPredicateWithSubpredicates: [
NSPredicate(format: "date = %#",Date().startOfDay as CVarArg),
NSPredicate(format: "startTime >= %#", Date() as CVarArg)
])
if let ev = try? TPEvent.fetchAll(predicates: predicates, in: persistentManager.context) {
events = ev
}
}
This event is called in viewWillAppear and widgetPerformUpdate.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
fetchEvents(date: Date())
self.tableView.reloadData()
}
func widgetPerformUpdate(completionHandler: (#escaping (NCUpdateResult) -> Void)) {
self.fetchEvents(date: Date() )
self.tableView.reloadData()
completionHandler(NCUpdateResult.newData)
}
persistentManaged.context is PersistentManager.shared.context (see code below).
By the way, both of the methods above are called when I view today-widget. I have a lot of time figuring out this issue but could not do so.
What could be the issue and how to fix it?
Please just comment should you need more info or have any question.
Update
I have a singleton PersistentManager. Use viewContext both in the host app and widget.
public final class PersistentManager {
init() {}
public static let shared = PersistentManager()
public lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentCloudKitContainer(name: "Event")
guard let fileContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.event.data") else {
fatalError("Shared file container could not be created.")
}
let storeURL = fileContainer.appendingPathComponent("Event.sqlite")
let storeDescription = NSPersistentStoreDescription(url: storeURL)
container.persistentStoreDescriptions = [storeDescription]
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
container.viewContext.automaticallyMergesChangesFromParent = true
do {
try container.viewContext.setQueryGenerationFrom(.current)
} catch {
fatalError("###\(#function): Failed to pin viewContext to the current generation:\(error)")
}
return container
}()
public lazy var context = persistentContainer.viewContext
// MARK: - Core Data Saving support
public 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)")
}
}
}
}
The issue is that the main app and the app extension work as two different processes on iOS.
CoreData works with NotificationCenter which sends notifications only within the main app process. Thus, you have to send interprocess notification here.
One hidden way to send interprocess notification on iOS is to use KVO on the UserDefaults object.
In NSUserDefaults.h header file Apple states that
/*!
NSUserDefaultsDidChangeNotification is posted whenever any user defaults changed within the current process, but is not posted when ubiquitous defaults change, or when an outside process changes defaults. Using key-value observing to register observers for the specific keys of interest will inform you of all updates, regardless of where they're from.
*/
Having this specified, one can assume that by using KVO on the particular key of UserDefaults, the value change will be propagated from the app to the extension, and vice versa.
So, the approach can be that on each change in the main app you save the current timestamp of the change into the UserDefaults:
/// When the change is made in the main app:
let defaults = UserDefaults(suiteName: "group.<your bundle id>")
defaults["LastChangeTimestamp"] = Date()
defaults.synchronize()
In the app extension:
let defaults = UserDefaults(suiteName: "group.<your bundle id>")
func subscribeForChangesObservation() {
defaults?.addObserver(self, forKeyPath: "LastChangeTimestamp", options: [.new, .initial], context: nil)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
// Process your changes here.
}
deinit {
defaults?.removeObserver(self, forKeyPath: "LastChangeTimestamp")
}
I have a project which already uses Core Data. I have added support for the iPad, but now will need to use iCloud & Core Data to sync the data.
I came across Ensembles, it seems like an easy and robust framework to add to my project. Found here: https://github.com/drewmccormack/ensembles
However there are no Swift example projects with the Ensembles project so have attempted to do it myself. Here are the steps I have taken,
Step 1
Manually add Ensembles to iOS projects.
Step 2
Create new CoreDataStack using existing persistent store .sql file.
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()
}
}
})
}
}
}
Step 3
Update App Delegate to sync and add notifications
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)
}
Step 4
Add Notifications to project to refresh 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
}
Step 5
Watch the magic happen.. Unfortunately there is no magic atm. The new CoreDataStack works fine, I can save and retrieve data from the persistent store.
I have two devices logged into the same iCloud account, and neither data is shared to the other device.
When deleting the app and reinstalling the data is not retrieved from the iCloud and saved to the persistent store.
I do get the following NSLog when 'sometimes' saving data or loading up the app.
2016-04-06 13:17:37.101 APPNAME[435:152241] Cloud data did download
This is the outcome for the following appDelegate notification function
func cloudDataDidDownload(notif: NSNotification) {
NSLog("Cloud data did download")
CoreDataStack.defaultStack.syncWithCompletion(nil)
}
The notification is sent from this function in the CoreDataStack for when changes are merged.
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)
})
}
}
So everything seems like it should be working fine. I get no errors but the data is not syncing. I cannot tell if the problem is the data backing up to iCloud or retrieving from iCloud and merging with the persistent store. All I can tell is that data is not shared between devices using the same iCloud account and the app does not actually restore the data from the iCloud when reinstalling the app.
Attached is my code below.
The line that is giving me the problems is let fetchRequest = try moc.executeFetchRequest(fetchRequest) as! [AppSettings] appears to load asynchronously but I want it to load synchronously so that I can ensure it checks properly for a username record.
How do I do this?
I know it loads asynchronously because when I start and stop the program constantly it will find the entity roughly 80% of the time and randomly 20% of the time it will not. Since nothing else is changing the entity (since I'm just starting and stopping the program constantly), it would make sense that the code is being run asynchrnously so when I use the command
guard let appSettingsArrayItem = fetchRequest.first where fetchRequest.count>0 else {
print ("no entities found...")
return false
}
It fails to find any entities sometimes.
Check Login Function
func checkIfLoggedInAlready() -> Bool{
let fetchRequest = NSFetchRequest(entityName: "AppSettings")
//let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) //Deletes ALL appsettings entities
do {
let fetchRequest = try moc.executeFetchRequest(fetchRequest) as! [AppSettings]
guard let appSettingsArrayItem = fetchRequest.first where fetchRequest.count>0 else {
print ("no entities found...")
return false
}
guard let username = (appSettingsArrayItem as AppSettings).username else{
print ("username not found")
return false
}
print("number Of AppSetting Entities =\(fetchRequest.count)")
print(username)
//The following code deletes ALL the entities!
//try moc.persistentStoreCoordinator!.executeRequest(deleteRequest, withContext: moc)
//To delete just '1' entry use the code below.
//moc.deleteObject(appSettingsArrayItem)
//try moc.save()//save deletion change.
//print("deleted particular entity item")
return true
} catch{
fatalError("bad things happened \(error)")
}
}
Entire LoginViewController including Check Login Function
import UIKit
import CoreData
class LoginViewController: UIViewController, UITextFieldDelegate {
#IBOutlet weak var usernameField: UITextField!
#IBOutlet weak var passwordField: UITextField!
var isLoggedIn = false
let moc = DataController().managedObjectContext
#IBAction func SignUpButtonPressed(sender: UIButton) {
print("sign up")
}
func textFieldShouldReturn(textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
func textFieldShouldEndEditing(textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
override func viewDidLoad() {
super.viewDidLoad()
let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: "dismissKeyboard")
view.addGestureRecognizer(tap)
print("view loaded, check if already signed in here")
let loggedIn = checkIfLoggedInAlready() //checks database to see
if(loggedIn){
print("was logged in!")
isLoggedIn = true
self.performSegueWithIdentifier("loginSegue", sender: self)
}
}
func checkIfLoggedInAlready() -> Bool{
let fetchRequest = NSFetchRequest(entityName: "AppSettings")
//let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) //Deletes ALL appsettings entities
do {
let fetchRequest = try moc.executeFetchRequest(fetchRequest) as! [AppSettings]
guard let appSettingsArrayItem = fetchRequest.first where fetchRequest.count>0 else {
print ("no entities found...")
return false
}
guard let username = (appSettingsArrayItem as AppSettings).username else{
print ("username not found")
return false
}
print("number Of AppSetting Entities =\(fetchRequest.count)")
print(username)
//The following code deletes ALL the entities!
//try moc.persistentStoreCoordinator!.executeRequest(deleteRequest, withContext: moc)
//To delete just '1' entry use the code below.
//moc.deleteObject(appSettingsArrayItem)
//try moc.save()//save deletion change.
//print("deleted particular entity item")
return true
} catch{
fatalError("bad things happened \(error)")
}
}
func dismissKeyboard() {
//Causes the view (or one of its embedded text fields) to resign the first responder status.
view.endEditing(true)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
// Get the new view controller using segue.destinationViewController.
// Pass the selected object to the new view controller.
print("prepare seque")
}
func displayErrorMessage(errorMessage: String){
print("show error console with Error:"+errorMessage)
let alert = UIAlertController(title: "Error", message: errorMessage, preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title: "Ok", style: UIAlertActionStyle.Default, handler: nil))
self.presentViewController(alert, animated: true, completion: nil)
}
override func shouldPerformSegueWithIdentifier(identifier: String, sender: AnyObject?) -> Bool {
switch(identifier){
case "loginSegue":
print("Is the user already logged in?")
if(isLoggedIn){
print("Detected as YES")
return true
}
print("Detected as NO, so checking username and password fields next...")
guard let password = passwordField.text!.stringByAddingPercentEncodingWithAllowedCharacters(.URLHostAllowedCharacterSet()) where !password.isEmpty else {
displayErrorMessage("Password can not be empty!")
return false
}
guard let username = usernameField.text!.stringByAddingPercentEncodingWithAllowedCharacters(.URLHostAllowedCharacterSet()) where !username.isEmpty else{
displayErrorMessage("Username can not be empty!")
return false
}
let url = "http://distribution.tech/restapi/v1/userlogin?email="+username+"&password="+password
print(url)
let json = JSON(url:url)
print(json)
if(json["status"].asInt==1){
let entity = NSEntityDescription.insertNewObjectForEntityForName("AppSettings", inManagedObjectContext: moc) as! AppSettings
entity.setValue(username, forKey: "username")
entity.setValue(password, forKey: "password")
entity.setValue(json["tokenid"].asString, forKey: "token")
entity.setValue(json["roleid"].asInt, forKey: "roleid")
entity.setValue(json["role"].asString, forKey: "role")
entity.setValue(json["companyid"].asInt , forKey: "companyid")
entity.setValue(json["isdev"].asInt, forKey: "isdev")
//save token and other details to database.
do {
try moc.save()
print("saved to entity")
}catch{
fatalError("Failure to save context: \(error)")
}
// token
// roleid int
// role
// companyid int
//
// {
// "companyid": 3,
// "userid": 2,
// "tokenid": "804febae26ddbd0292b3d2c66b30afd5028d5ba9",
// "status": 1,
// "roleId": 1,
// "role": "super_admin",
// "isdev": 0
// }
//Save to disk using our own method, as COREDATA is unreliable!
return true //login succesfull
}else{
displayErrorMessage("Incorrect Username or Email")
return false//failed
}
default:
displayErrorMessage("Unknown Error Related To Segue Not Found")
}
return false //if it gets to this point assume false
}
}
The managed object is created in the DataController its file is here below.
import UIKit
import CoreData
class DataController: NSObject {
var managedObjectContext: NSManagedObjectContext
override init() {
// This resource is the same name as your xcdatamodeld contained in your project.
guard let modelURL = NSBundle.mainBundle().URLForResource("AppSettings", withExtension:"momd") else {
fatalError("Error loading model from bundle")
}
// The managed object model for the application. It is a fatal error for the application not to be able to find and load its model.
guard let mom = NSManagedObjectModel(contentsOfURL: modelURL) else {
fatalError("Error initializing mom from: \(modelURL)")
}
let psc = NSPersistentStoreCoordinator(managedObjectModel: mom)
self.managedObjectContext = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType)
self.managedObjectContext.persistentStoreCoordinator = psc
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)) {
let urls = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)
let docURL = urls[urls.endIndex-1]
/* The directory the application uses to store the Core Data store file.
This code uses a file named "DataModel.sqlite" in the application's documents directory.
*/
let storeURL = docURL.URLByAppendingPathComponent("AppSettings.sqlite")
do {
try psc.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: storeURL, options: nil)
} catch {
fatalError("Error migrating store: \(error)")
}
}
}
}
Image Reference Of Entity & Console Error That Can Happen Sometimes
Image Reference Of Entity & Console When It Does Find Entity Most Of Time
ManagedObjectContext.ExecuteFetchRequest already runs synchronously but it looks like you are setting up your persistent store coordinator asynchronously in a background priority thread.
If this fetch request happens immediately when the app starts up, and you do it over and over again, it may not be finished setting up some of the times.
Okay the answer above was correct, so what I did was created a new project single view, selected core data option, and copied code from its AppDelegate over my own AppDelegate to get the proper CoreData Init Code, and in such a way that when the project terminates it saves the context correctly and so forth. The code looks like this.
import UIKit
import CoreData
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Override point for customization after application launch.
return true
}
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 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.
}
func applicationWillEnterForeground(application: UIApplication) {
// Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
}
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.
}
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.
self.saveContext()
}
// MARK: - Core Data stack
lazy var applicationDocumentsDirectory: NSURL = {
// The directory the application uses to store the Core Data store file. This code uses a directory named "com.distribution.tech.Test" 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("AppSettings", 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(managedObjectModel: self.managedObjectModel)
let url = self.applicationDocumentsDirectory.URLByAppendingPathComponent("AppSettings.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
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()
}
}
}
}
Its key when you do this that you change the reference to your own xcdatamodeld or this won't work. In my case it was changing this line to the correct sqlite based on my previous work.
let url = self.applicationDocumentsDirectory.URLByAppendingPathComponent("AppSettings.sqlite")
and this line...
let modelURL = NSBundle.mainBundle().URLForResource("AppSettings", withExtension: "momd")!
which is the actual name of the xcdatamodeld file.
Hope this helps someone who had same issue as me. Oh...and apple if you are reading this...please add 'core data' option for tab based projects in the future... and not just single view.
What I am trying to achieve is when user creates, changes or deletes data on the persistent store it will be synced with the iCloud store and then update other devices logged into the same iCloud account.
I have created a Core Data Stack using Objective-C resources and tried to write my own in Swift but I am having problems syncing the data using two devices logged into the same iCloud account.
For e.g. when iDevice A is logged into iCloud it will back up data to iCloud but when iDevice B logs into iCloud the app deletes any data already on the persistent storage to save the iCloud back up, and any changes between the store devices does not appear on the other but seem to save to the iCloud store as the latest back up, so if the app is deleted and reinstalled I see the latest backup made by the other device - with this in mind, if iDevice B was already logged in, it would not use data from iDevice A unless the app was reinstalled and the last back up was made by the other device.
Does anybody know where I am going wrong in the Core data stack to sync the data between two devices using the same iCloud account ?
Core Data Stack:
// MARK: - Core Data stack
// All the following code is in my appDelgate Core data stack
func observeCloudActions(persistentStoreCoordinator psc: NSPersistentStoreCoordinator?) {
// Register iCloud notifications observers for;
//Stores Will change
//Stores Did Change
//persistentStoreDidImportUbiquitousContentChanges
//mergeChanges
}
//Functions for notifications
func mergeChanges(notification: NSNotification) {
NSLog("mergeChanges notif:\(notification)")
if let moc = managedObjectContext {
moc.performBlock {
moc.mergeChangesFromContextDidSaveNotification(notification)
self.postRefetchDatabaseNotification()
}
}
}
func persistentStoreDidImportUbiquitousContentChanges(notification: NSNotification) {
self.mergeChanges(notification);
}
func storesWillChange(notification: NSNotification) {
NSLog("storesWillChange notif:\(notification)");
if let moc = self.managedObjectContext {
moc.performBlockAndWait {
var error: NSError? = nil;
if moc.hasChanges && !moc.save(&error) {
NSLog("Save error: \(error)");
} else {
// drop any managed objects
}
moc.reset();
}
NSNotificationCenter.defaultCenter().postNotificationName("storeWillChange", object: nil)
}
}
func storesDidChange(notification: NSNotification) {
NSLog("storesDidChange posting notif");
self.postRefetchDatabaseNotification();
//Sends notification to view controllers to reload data NSNotificationCenter.defaultCenter().postNotificationName("storeDidChange", object: nil)
}
func postRefetchDatabaseNotification() {
dispatch_async(dispatch_get_main_queue(), { () -> Void in
NSNotificationCenter.defaultCenter().postNotificationName("storeDidChange", object: nil)
})
}
// Core data stack
lazy var applicationDocumentsDirectory: NSURL = {
// The directory the application uses to store the Core Data store file. This code uses a directory named "hyouuu.pendo" in the application's documents Application Support directory.
let urls = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)
return urls[urls.count-1] as! NSURL
}()
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("AppName", withExtension: "momd")!
NSLog("modelURL:\(modelURL)")
return NSManagedObjectModel(contentsOfURL: modelURL)!
}()
lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator? = {
// The persistent store coordinator for the application. This implementation creates and return 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
var coordinator: NSPersistentStoreCoordinator? = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)
let documentsDirectory = NSFileManager.defaultManager().URLsForDirectory(NSSearchPathDirectory.DocumentDirectory, inDomains: NSSearchPathDomainMask.UserDomainMask).last as! NSURL
let storeURL = documentsDirectory.URLByAppendingPathComponent("CoreData.sqlite")
NSLog("storeURL:\(storeURL)")
let storeOptions = [NSPersistentStoreUbiquitousContentNameKey:"AppStore"]
var error: NSError? = nil
var failureReason = "There was an error creating or loading the application's saved data."
if coordinator!.addPersistentStoreWithType(
NSSQLiteStoreType,
configuration: nil,
URL: storeURL,
options: storeOptions,
error: &error) == nil
{
coordinator = nil
// Report any error we got.
let dict = NSMutableDictionary()
dict[NSLocalizedDescriptionKey] = "Failed to initialize the application's saved data"
dict[NSLocalizedFailureReasonErrorKey] = failureReason
dict[NSUnderlyingErrorKey] = error
error = NSError(domain: "Pendo_Error_Domain", code: 9999, userInfo: dict as [NSObject : AnyObject])
// 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("AddPersistentStore error \(error), \(error!.userInfo)")
}
self.observeCloudActions(persistentStoreCoordinator: coordinator)
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
if coordinator == nil {
return nil
}
var managedObjectContext = NSManagedObjectContext(concurrencyType: NSManagedObjectContextConcurrencyType.MainQueueConcurrencyType)
managedObjectContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
managedObjectContext.persistentStoreCoordinator = coordinator
return managedObjectContext
}()
This how I have mine setup and it syncs my core data and it keeps changes synced.
This from my AppDelegate.
lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator? = {
// The persistent store coordinator for the application. This implementation creates and return 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
var coordinator: NSPersistentStoreCoordinator? = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)
let url = self.applicationDocumentsDirectory.URLByAppendingPathComponent("APPNAME.sqlite")
var error: NSError? = nil
var failureReason = "There was an error creating or loading the application's saved data."
// iCloud store
var storeOptions = [NSPersistentStoreUbiquitousContentNameKey : "APPNAMEStore",NSMigratePersistentStoresAutomaticallyOption: true,
NSInferMappingModelAutomaticallyOption: true]
// iCloud storeOptions need to be added to the if statement
do {
try coordinator!.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: NSURL.fileURLWithPath(url.path!), options: storeOptions)
} catch var error1 as NSError {
error = error1
coordinator = nil
// 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
error = 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 \(error), \(error!.userInfo)")
abort()
} catch {
fatalError()
}
return coordinator
}()
// MARK: - iCloud
// This handles the updates to the data via iCLoud updates
func registerCoordinatorForStoreNotifications (coordinator : NSPersistentStoreCoordinator) {
let nc : NSNotificationCenter = NSNotificationCenter.defaultCenter();
nc.addObserver(self, selector: "handleStoresWillChange:",
name: NSPersistentStoreCoordinatorStoresWillChangeNotification,
object: coordinator)
nc.addObserver(self, selector: "handleStoresDidChange:",
name: NSPersistentStoreCoordinatorStoresDidChangeNotification,
object: coordinator)
nc.addObserver(self, selector: "handleStoresWillRemove:",
name: NSPersistentStoreCoordinatorWillRemoveStoreNotification,
object: coordinator)
nc.addObserver(self, selector: "handleStoreChangedUbiquitousContent:",
name: NSPersistentStoreDidImportUbiquitousContentChangesNotification,
object: coordinator)
}
A difference I see between what works for me and your code is:
1) I don't see where you've added an observer for the NSPersistentStoreDidImportUbiquitousContentChangesNotification such as in the code:
let nc : NSNotificationCenter = NSNotificationCenter.defaultCenter();
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
nc.addObserver(self, selector: "dataUpdated:",
name: NSPersistentStoreDidImportUbiquitousContentChangesNotification,
object: self.managedObjectContext?.persistentStoreCoordinator)
2) In my code in the function that catches the notification (dataUpdated in this case), it resets the managed objext context with the following code before it updates the display to show the new data:
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
if let managedObjectContext = appDelegate.managedObjectContext {
managedObjectContext.reset()
}
After resetting the managed object context, I fetch the entity again using the code:
let fetchRequest = NSFetchRequest(entityName: "ClassNameHere")
let sortDescriptor = NSSortDescriptor(key: "name", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
let fetchResults = managedObjectContext!.executeFetchRequest(fetchRequest, error: nil) as! [ClassNameHere]
I'm trying to understand CloudKit, but I'm having an issue figuring out a real-world issue if I eventually turned it into an app.
I've got a tableviewcontroller that's populated by CloudKit, which works fine. I've got a detailviewcontroller that is pushed when you tap one of the entries in the tableview controller. The detailviewcontroller pulls additional data from CloudKit that wasn't in the initial tableviewcontroller. All of this works perfectly fine.
Now, the detailviewcontroller allows changing of an image and saving it back to a public database. That's working fine as well.
The scenario I'm dealing with is thinking about this core function being used in an app by multiple people. If one person uploads a photo (overwriting an existing photo for one of the records), what happens if another user currently has the app in the background? If they open the app, they'll see the last photo they saw on that page, and not the new photo. So I want to reload the data from CloudKit when the app comes back to the foreground, but I must be doing something wrong, because it's not working. Using the simulator and my actual phone, I can change a photo on one device and the other device (simulator or phone) doesn't update the data when it comes into the foreground.
Here's the code I'm using: First in ViewController.swift:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
println("viewDidLoad")
NSNotificationCenter.defaultCenter().addObserver(self, selector: "activeAgain", name: UIApplicationWillEnterForegroundNotification, object: nil)
and then the activeAgain function:
func activeAgain() {
println("Active again")
fetchItems()
self.tblItems.reloadData()
println("Table reloaded")
}
and then the fetchItems function:
func fetchItems() {
let container = CKContainer.defaultContainer()
let publicDatabase = container.publicCloudDatabase
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: "Items", predicate: predicate)
publicDatabase.performQuery(query, inZoneWithID: nil) { (results, error) -> Void in
if error != nil {
println(error)
}
else {
println(results)
for result in results {
self.items.append(result as! CKRecord)
}
NSOperationQueue.mainQueue().addOperationWithBlock({ () -> Void in
self.tblItems.reloadData()
self.tblItems.hidden = false
})
}
}
}
I tried adding a reload function to viewWillAppear, but that didn't really help:
override func viewWillAppear(animated: Bool) {
tblItems.reloadData()
}
Now here's the code in DetailViewController.swift:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
NSNotificationCenter.defaultCenter().addObserver(self, selector: "activeAgain", name: UIApplicationWillEnterForegroundNotification, object: nil)
self.imageScrollView.contentSize = CGSizeMake(1000, 1000)
self.imageScrollView.frame = CGRectMake(0, 0, 1000, 1000)
self.imageScrollView.minimumZoomScale = 1.0
self.pinBoardScrollView.maximumZoomScale = 8.0
showImage()
and then the showImage function:
func showImage() {
imageView.hidden = true
btnRemoveImage.hidden = true
viewWait.hidden = true
if let editedImage = editedImageRecord {
if let imageAsset: CKAsset = editedImage.valueForKey("image") as? CKAsset {
imageView.image = UIImage(contentsOfFile: imageAsset.fileURL.path!)
imageURL = imageAsset.fileURL
self.title = "Image"
imageView.hidden = false
btnRemoveImage.hidden = false
btnSelectPhoto.hidden = true
}
}
}
and the activeAgain function:
func activeAgain() {
showImage()
println("Active again")
}
Neither of these ViewControllers reload the data from CloudKit when they return. It's like it's cached the table data for the first ViewController and the image for the DetailViewController and it's using that instead of downloading the actual CloudKit data. I'm stumped, so I figure I better try this forum (my first post!). Hopefully I included enough required info to be useful.
Your showImage function is using a editedImage CKRecord. Did you also reload that record? Otherwise the Image field in that record would then still contain the old CKAsset.
Besides just reloading on a moment you feel right, you could also let the changes be pushed by creating a CloudKit subscription. Then your data will even change when it's active and changed by someone else.