diff --git a/.gitignore b/.gitignore index 681dd3c..56e4db0 100644 --- a/.gitignore +++ b/.gitignore @@ -2,14 +2,8 @@ .DS_Store # IntelliJ -/.idea/* -!/.idea/encodings.xml -!/.idea/inspectionProfiles -!/.idea/projectCodeStyle.xml -!/.idea/validation.xml -*.iml -/*.ipr -/*.iws +.idea/workspace.xml +.idea/dictionaries/ # Xcode IDE /*.xcodeproj/* diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..1e11905 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "jrswizzle"] + path = jrswizzle + url = git://github.com/jonmarimba/jrswizzle.git diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..a147236 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +UbiquityStoreManagerExample \ No newline at end of file diff --git a/.idea/codeStyleSettings.xml b/.idea/codeStyleSettings.xml new file mode 100644 index 0000000..19e9b07 --- /dev/null +++ b/.idea/codeStyleSettings.xml @@ -0,0 +1,90 @@ + + + + + + + diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..e206d70 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/.idea/find.xml b/.idea/find.xml new file mode 100644 index 0000000..6892330 --- /dev/null +++ b/.idea/find.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/.idea/iCloudStoreManager.iml b/.idea/iCloudStoreManager.iml new file mode 100644 index 0000000..f9b36de --- /dev/null +++ b/.idea/iCloudStoreManager.iml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..ddf5da5 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..3b31283 --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..806e25c --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..f190de7 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/.idea/runConfigurations/UbiquityStoreManagerExample.xml b/.idea/runConfigurations/UbiquityStoreManagerExample.xml new file mode 100644 index 0000000..44315c7 --- /dev/null +++ b/.idea/runConfigurations/UbiquityStoreManagerExample.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/scopes/scope_settings.xml b/.idea/scopes/scope_settings.xml new file mode 100644 index 0000000..922003b --- /dev/null +++ b/.idea/scopes/scope_settings.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..c9de466 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/.idea/xcode.xml b/.idea/xcode.xml new file mode 100644 index 0000000..5dc4592 --- /dev/null +++ b/.idea/xcode.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/LICENSE b/LICENSE index a934f0f..65c5ca8 100644 --- a/LICENSE +++ b/LICENSE @@ -1,12 +1,165 @@ -Copyright (c) 2012, Yodel Code LLC -All rights reserved. + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. -Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. -Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. -Neither the name of Yodel Code LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + 0. Additional Definitions. -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL YODEL CODE LLC OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/README b/README deleted file mode 100644 index 0a88891..0000000 --- a/README +++ /dev/null @@ -1,62 +0,0 @@ - -iCloudStoreManager Notes - Created April 4, 2012 - -INTRODUCTION - -This standalone iOS project demonstrates using iCloud to sync a CoreData store between different iOS devices, what is often referred to as a Shoebox-style app. It must be run on an actual iOS device. - -WARNING: This code continues to display occasional errors and may not yet be suitable for production release. - -USING THE APP - -To run the app you will first need to setup the iCloud entitlements. If you are not familiar with this, there is an excellent discussion here: http://oleb.net/blog/2011/11/ios5-tech-talk-michael-jurewitz-on-icloud-storage/ - -After installing the app on at least two devices. Generate a few lines of data by tapping the + symbol. Then tap the iCloud switch to ON. After anywhere from 10 seconds to 10 minutes you should see the second device's iCloud switch automatically switch ON, and the data on both devices should now be the same. Add or delete data by swiping on either device to observe how the data stays in sync. However, be prepared to see propagation delays that can be many minutes. - -You can also try switching iCloud OFF and ON on either device to see how it can easily be switched between local data and iCloud data. - -THE UX MODEL - -The user experience model follows a very simple set of rules. An iCloud switch is required so that users can control from which device they seed iCloud. This is important because once an iCloud store is seeded with data, it should not be seeded again. - -1. A user can seed iCloud with a baseline set of data from any device. -2. Once iCloud is seeded from one device, other devices are automatically enabled to use iCloud using the seeded data. This may take anywhere from 10 seconds to 10 minutes. My experience with iCloud is that it is very slow. -3. If a user attempts to seed iCloud from multiple devices at the same time, the last device to seed iCloud with data wins. -4. A user can at any time disable iCloud from within the app. That will switch the app from the iCloud store to the local data store. These stores are completely separate and independent. -5. A user can at any time enable iCloud from within the app. That will switch the app from using the local data store to the iCloud store. - -For test purposes ONLY, tapping the Clear iCloud button will delete all iCloud data from all devices (it may take many seconds or many minutes for this action to propagate to other devices). When this happens, each individual device will switch to using it's local data, and iCloud within the app is disabled. - -USING UbiquityStoreManager - -The goal was to implement the simplest possible API. Apple's instructions on how to use Core Data with iCloud are deceivingly simple. Hopefully this class will make it truly simple and straightforward. - -To implement this same functionality in your own app, all you need is UbiquityStoreManager.h and UbiquityStoreManager.m. There are roughly 7+ steps required in configuring and using UbiquityStoreManager. Search for STEP to find examples of all of the changes you will likely need to make it work in your own project. - -This class includes alert messages to assist the user in making the appropriate choices when enabling or disabling iCloud. - -The three most important methods are listed here: - -1. Use this method to create and initialize a single instance in your app delegate class: - -- (id)initWithManagedObjectModel:(NSManagedObjectModel *)model localStoreURL:(NSURL *)storeURL; - -2. Instead of creating your own persistent coordinator, you must use the one supplied by UbiquityStoreManager. - -- (NSPersistentStoreCoordinator *)persistentStoreCoordinator; - -3. And to switch between using the iCloud store and the local data store, call this method. - -- (void)useiCloudStore:(BOOL)willUseiCloud; - -However, using just these three methods is not sufficient for Core Data to function properly with iCloud. Please look at the source code to see all of the other methods and properties required. - -TESTING AND KNOWN PROBLEMS - -This has been tested out on an iPhone 4, iPhone 4S, original iPad, and the new iPad. Everything has been tested with up to four devices and works well in most cases. However, changes may sometimes take 20 minutes to propagate (typically on my new iPad), leading you to think it's not working. But it does eventually catch up. A bug report has been filed and is referenced at http://openradar.appspot.com/radar?id=1643401 - - -iCloudStoreManager is released under the New BSD License. - -Copyright (c) 2012, Yodel Code LLC -All rights reserved. diff --git a/README.markdown b/README.markdown new file mode 100644 index 0000000..8e1afa2 --- /dev/null +++ b/README.markdown @@ -0,0 +1,124 @@ +# About + +`UbiquityStoreManager` is a controller that implements iCloud integration with Core Data for you. + +While Apple portrays iCloud integration as trivial, the contrary is certainly true. Especially for Core Data, there are many caveats, side-effects and undocumented behaviors that need to be handled to get a reliable implementation. + +Unfortunately, Apple also has a bunch of serious bugs left to work out in this area, which can sometimes lead to cloud stores that become desynced or even irreparably broken. `UbiquityStoreManager` handles these situations as best as possible. + +The API has been kept as simple as possible while giving you, the application developer, the hooks you need to get the behavior you want. Wherever possible, `UbiquityStoreManager` implements safe and sane default behavior to handle exceptional situations. These cases are well documented in the API documentation, as well as your ability to plug into the manager and implement your own custom behavior. + +# Getting Started + +To get started with `UbiquityStoreManager`, all you need to do is instantiate it: + + [[UbiquityStoreManager alloc] initStoreNamed:nil + withManagedObjectModel:nil + localStoreURL:nil + containerIdentifier:nil + additionalStoreOptions:nil + delegate:self] + +And then wait in your delegate for the manager to bring up your persistence layer: + + - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager willLoadStoreIsCloud:(BOOL)isCloudStore { + + self.moc = nil; + } + + - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didLoadStoreForCoordinator:(NSPersistentStoreCoordinator *)coordinator isCloud:(BOOL)isCloudStore { + self.moc = [[NSManagedObjectContext alloc] + initWithConcurrencyType:NSMainQueueConcurrencyType]; + [moc setPersistentStoreCoordinator:coordinator]; + [moc setMergePolicy:NSMergeByPropertyObjectTrumpMergePolicy]; + } + +That’s it! The manager set up your `NSPersistentStoreCoordinator`, you created an `NSManagedObjectContext`, you’re ready to go. + +Just keep in mind, as aparent from the code above, that your `moc` can be `nil`. This happens when the manager is not (yet) ready with loading the store. It can also occur after the store has been loaded (eg. cloud is turned on/off or the store needs to be re-loaded). So just make sure that your application deals gracefully with your main `moc` being unavailable. + +Initially, the manager will be using a local store. To enable iCloud (you may want to do this after the user toggles iCloud on), just flip the switch: + + manager.cloudEnabled = YES; + +# Surely I’m not done yet! + +That depends on how much you want to get involved with what `UbiquityStoreManager` does internally to handle your store, and how much feedback you want to give your user with regards to what’s going on. + +For instance, you may want to implement visible feedback for while persistence is unavailable (eg. show an overlay with a loading spinner). You’d bring this spinner up in `-ubiquityStoreManager:willLoadStoreIsCloud:` and dismiss it in `-ubiquityStoreManager:didLoadStoreForCoordinator:isCloud:`. + +It’s probably also a good idea to update your main `moc` whenever ubiquity changes are getting imported into your store from other devices. To do this, simply provide the manager with your `moc` by returning it from `-managedObjectContextForUbiquityChangesInManager:` and optionally register an observer for `UbiquityManagedStoreDidImportChangesNotification`. + +# What if things go wrong? + +And don’t be fooled: Things do go wrong. Apple has a few kinks to work out, some of these can cause the cloud store to become irreparably desynced. + +`UbiquityStoreManager` does its best to deal with these issues, mostly automatically. Because the manager takes great care to ensure no data-loss occurs there are some rare cases where the store cannot be automatically salvaged. It is therefore important that you implement some failure handling, at least in the way recommended by the manager. + +While it theoretically shouldn’t happen, sometimes ubiquity changes designed to sync your cloud store with the store on other devices can be incompatible with your cloud store. Usually, this happens due to an Apple bug in dealing with relationships that are simultaneously edited from different devices, causing conflicts that can’t be handled. Interestingly, the errors happen deep within Apple’s iCloud implementation and doesn’t bother to notify you through any public API. `UbiquityStoreManager` implements a way of detecting these issues when they occur and deals with them as best as it can. + +Whenever problems occur with importing transaction logs (ubiquity changes), your application can be notified and optionally intervene by implementing `-ubiquityStoreManager:handleCloudContentCorruptionWithHealthyStore:` in your delegate. If you just want to be informed and let the manager handle the situation, return `NO`. If you want to handle the situation in a different way than what the manager does by default, return `YES` after dealing with the problem yourself. + +Essentially, the manager deals with import exceptions by unloading the store on the device where ubiquity changes conflict with the store and notifying all other devices that the store has entered a **”corrupted”** state. Other devices may not experience any errors (they may be the authors of the corrupting logs, or they may not experience conflicts between their store and the logs). When any of these **healthy** devices receive word of the store corruption, they will initiate a store rebuild causing a brand new cloud store to be created populated by the old cloud store’s entities. At this point, all devices will switch over to the new cloud store and the corruption state will be cleared. + +You are recommended to implement `-ubiquityStoreManager:handleCloudContentCorruptionWithHealthyStore:` by returning `NO` but informing the user of what is going on. Here’s an example implementation that displays an alert for the user if his device needs to wait for another device to fix the corruption: + + - (BOOL)ubiquityStoreManager:(UbiquityStoreManager *)manager + handleCloudContentCorruptionWithHealthyStore:(BOOL)storeHealthy { + + if (![self.cloudAlert isVisible] && manager.cloudEnabled && !storeHealthy) + dispatch_async( dispatch_get_main_queue(), ^{ + self.cloudAlert = [[UIAlertView alloc] + initWithTitle:@"iCloud Sync Problem" + message:@"\n\n\n\n" + @"Waiting for another device to auto‑correct the problem..." + delegate:self + cancelButtonTitle:nil otherButtonTitles:@"Fix Now", nil]; + UIActivityIndicatorView *activityIndicator = [[UIActivityIndicatorView alloc] + initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge]; + activityIndicator.center = CGPointMake( 142, 90 ); + [activityIndicator startAnimating]; + [self.cloudAlert addSubview:activityIndicator]; + [self.cloudAlert show]; + } ); + + return NO; + } + +The above code gives the user the option of hitting the `Fix Now` button, which would invoke `[manager rebuildCloudContentFromCloudStoreOrLocalStore:YES]`. Essentially, it initiates the cloud store rebuild locally. More about this later. + +Your app can now deal with Apple’s iCloud bugs, congratulations! + +Unless you want to get into the deep water, ***you’re done now***. What follows is for brave souls or those seeking for maximum control. + +# What else have you got? + +Since this is murky terrain, `UbiquityStoreManager` tries its best to keep interested delegates informed of what’s going on, and even gives it the ability to intervene in non-standard ways. + +If you use a logger, you can plug it in by implementing `-ubiquityStoreManager:log:`. This method is called whenever the manager has something to say about what it’s doing. We’re pretty verbose, so you may even want to implement this just to shut the manager up in production. + +If you’re interested in getting the full details about any error conditions, implement `-ubiquityStoreManager:didEncounterError:cause:context:` and you shall receive. + +If the cloud content gets deleted, the manager unloads the persistence stores. This may happen, for instance, if the user has gone into `Settings` and deleted the iCloud data for your app, possibly in an attempt to make space on his iCloud account. By default, this will leave your app without any persistence until the user restarts the app. If iCloud is still enabled in the app, a new store will be created for him. You could handle this a little differently, depending on what you think is right: You may want to just display a message to the user asking him whether he wants iCloud disabled or re-enabled. Or you may want to just disable iCloud and switch to the local store. You would handle this from `-ubiquityStoreManagerHandleCloudContentDeletion:`. + +If you read the previous section carefully, you should understand that problems may occur during the importing of ubiquitous changes made by other devices. The default way of handling the situation can usually automatically resolve the situation but may take some time to completely come about and may involve user interaction. You may choose to handle the situation differently by implementing `-ubiquityStoreManager:handleCloudContentCorruptionWithHealthyStore:` and returning `YES` after dealing with the corruption yourself. The manager provides the following methods for you, which you can use for some low-level maintenance of the stores: + * `-reloadStore` — Just clear and re-open or retry opening the active store. + * `-deleteCloudContainerLocalOnly:` — All iCloud data for your application will be deleted. That’s ***not just your Core Data store***! + * `-deleteCloudStoreLocalOnly:` — Your Core Data cloud store will be deleted. + * `-deleteLocalStore` — This will delete your local Core Data store (ea. the store that’s active when `manager.cloudEnabled = NO`). + * `-migrateCloudToLocalAndDeleteCloudStoreLocalOnly:` — Use this method to stop using iCloud but migrate all cloud data into the local store. + * `-rebuildCloudContentFromCloudStoreOrLocalStore:` — This is where the cloud store rebuild magic happens. Invoke this method to create a new cloud store and copy your current cloud data into it. + +Many of these methods take a `localOnly` parameter. Set it to `YES` if you don’t want to affect the user’s iCloud data. The operation will happen on the local device only. For instance, if you run `[manager deleteCloudStoreLocalOnly:YES]`, the cloud store on the device will be deleted. If `cloudEnabled` is `YES`, the manager will subsequently re-open the cloud store which will cause a re-download of all iCloud’s transaction logs for the store. These transaction logs will then get replayed locally causing your local store to be repopulated from what’s in iCloud. + +# Disclaimer + +I provide `UbiquityStoreManager` and its example application to you for free and do not take any responsability for what it may do in your application. + +# License + +`UbiquityStoreManager` is licensed under the LGPLv3. Feel free to use it in any of your applications. I’m also happy to receive any comments, feedback or review any pull requests. + +Creating `UbiquityStoreManager` has taken me a huge amount of work and few developers have so far been brave enough to try and solve the iCloud for Core Data problem that Apple left us with. If this solution is useful to you, please consider saying thanks or donating to the cause. + +Click here to lend your support to: UbiquityStoreManager and make a donation at www.pledgie.com ! \ No newline at end of file diff --git a/UbiquityStoreManager/NSError+UbiquityStoreManager.h b/UbiquityStoreManager/NSError+UbiquityStoreManager.h new file mode 100644 index 0000000..85dfcf3 --- /dev/null +++ b/UbiquityStoreManager/NSError+UbiquityStoreManager.h @@ -0,0 +1,17 @@ +// +// Created by lhunath on 2013-03-13. +// +// To change the template use AppCode | Preferences | File Templates. +// + + +#import + + +extern NSString *const UbiquityManagedStoreDidDetectCorruptionNotification; + +@interface NSError(UbiquityStoreManager) + +- (id)init_USM_WithDomain:(NSString *)domain code:(NSInteger)code userInfo:(NSDictionary *)dict; + +@end diff --git a/UbiquityStoreManager/NSError+UbiquityStoreManager.m b/UbiquityStoreManager/NSError+UbiquityStoreManager.m new file mode 100644 index 0000000..98df438 --- /dev/null +++ b/UbiquityStoreManager/NSError+UbiquityStoreManager.m @@ -0,0 +1,24 @@ +// +// Created by lhunath on 2013-03-13. +// +// To change the template use AppCode | Preferences | File Templates. +// + + +#import "NSError+UbiquityStoreManager.h" + + +NSString *const UbiquityManagedStoreDidDetectCorruptionNotification = @"UbiquityManagedStoreDidDetectCorruptionNotification"; + +@implementation NSError(UbiquityStoreManager) + +- (id)init_USM_WithDomain:(NSString *)domain code:(NSInteger)code userInfo:(NSDictionary *)dict { + + self = [self init_USM_WithDomain:domain code:code userInfo:dict]; + if ([domain isEqualToString:NSCocoaErrorDomain] && code == 134302) + [[NSNotificationCenter defaultCenter] postNotificationName:UbiquityManagedStoreDidDetectCorruptionNotification object:self]; + + return self; +} + +@end diff --git a/UbiquityStoreManager/UbiquityStoreManager.h b/UbiquityStoreManager/UbiquityStoreManager.h new file mode 100644 index 0000000..9127e38 --- /dev/null +++ b/UbiquityStoreManager/UbiquityStoreManager.h @@ -0,0 +1,334 @@ +// +// UbiquityStoreManager.h +// UbiquityStoreManager +// +// UbiquityStoreManager is a controller for your Core Data persistence layer. +// It provides you with an NSPersistentStoreCoordinator and handles the stores for you. +// It encapsulates everything required to make Core Data integration with iCloud work as reliably as possible. +// +// Aside from this, it features the following functionality: +// +// - Ability to switch between a separate cloud-synced and local store (an iCloud toggle). +// - Automatically migrates local data to iCloud when the user has no iCloud store yet. +// - Handles all iCloud related events such as: +// - Account changes +// - External deletion of the cloud data +// - External deletion of the local store +// - Importing of ubiquitous changes from other devices +// - Recovering from exceptional events such as corrupted transaction logs +// - Some maintenance functionality: +// - Ability to rebuild the cloud store from transaction logs +// - Ability to delete the cloud store (allowing it to be recreated from the local store) +// - Ability to nuke the entire cloud container +// +// Known issues: +// - Sometimes Apple's iCloud implementation hangs itself coordinating access for importing ubiquitous changes. +// - Reloading the store with -reloadStore can sometimes cause these changes to get imported. +// - If not, the app needs to be restarted. +// - Sometimes Apple's iCloud implementation will write corrupting transaction logs to the cloud container. +// - As a result, all other devices will fail to import any future changes to the store. +// - The only remedy is to recreate the store. +// - TODO: This manager allows the cloud store to be recreated and seeded by the old cloud store. +// + +#import +#import + + +/** + * The store managed by the ubiquity manager's coordinator changed (eg. switching (no store) or switched to iCloud or local). + * + * This notification is posted after the -ubiquityStoreManager:willLoadStoreIsCloud: or -ubiquityStoreManager:didLoadStoreForCoordinator:isCloud: message was posted to the delegate. + */ +extern NSString *const UbiquityManagedStoreDidChangeNotification; +/** + * The store managed by the ubiquity manager's coordinator imported changes from iCloud (eg. another device saved changes to iCloud). + */ +extern NSString *const UbiquityManagedStoreDidImportChangesNotification; + +typedef enum { + UbiquityStoreErrorCauseNoError, // Nothing went wrong. There is no context. + UbiquityStoreErrorCauseNoAccount, // The user is not logged into iCloud on this device. There is no context. + UbiquityStoreErrorCauseDeleteStore, // Error occurred while deleting the store file or its transaction logs. context = the path of the store. + UbiquityStoreErrorCauseCreateStorePath, // Error occurred while creating the path where the store needs to be saved. context = the path of the store. + UbiquityStoreErrorCauseClearStore, // Error occurred while removing a store from the coordinator. context = the store. + UbiquityStoreErrorCauseOpenActiveStore, // Error occurred while opening the active store. context = the path of the store. + UbiquityStoreErrorCauseOpenSeedStore, // Error occurred while opening the seed store. context = the path of the store. + UbiquityStoreErrorCauseSeedStore, // Error occurred while seeding the store. context = the path of the seed store. + UbiquityStoreErrorCauseImportChanges, // Error occurred while importing changes from the cloud into the application's context. context = the DidImportUbiquitousContentChanges notification. +} UbiquityStoreErrorCause; + +typedef enum { + UbiquityStoreMigrationStrategyCopyEntities, // Migrate by copying all entities from the active store to the new store. + UbiquityStoreMigrationStrategyIOS, // Migrate using iOS' migration routines (bugged for: cloud -> local on iOS 6.0, local -> cloud on iOS 6.1). + UbiquityStoreMigrationStrategyManual, // Migrate using the delegate's -ubiquityStoreManager:manuallyMigrateStore:toStore:. + UbiquityStoreMigrationStrategyNone, // Don't migrate, just create an empty destination store. +} UbiquityStoreMigrationStrategy; + +@class UbiquityStoreManager; + +@protocol UbiquityStoreManagerDelegate + +/** When cloud changes are detected, the manager can merge these changes into your managed object context. + * + * If you don't implement this method or return nil, the manager will commit the changes to the store + * (using NSMergeByPropertyObjectTrumpMergePolicy) but your application may not become aware of them. + * + * If you do implement this method, the changes will be merged into your managed object context + * and the context will be saved afterwards. + * + * Regardless of whether this method is implemented or not, a UbiquityManagedStoreDidImportChangesNotification will be + * posted after the changes are successfully imported into the store. + */ +@optional +- (NSManagedObjectContext *)managedObjectContextForUbiquityChangesInManager:(UbiquityStoreManager *)manager; + +/** Triggered when the store manager begins loading a persistence store. + * + * Between this and an invocation of -ubiquityStoreManager:didLoadStoreForCoordinator:isCloud: or -ubiquityStoreManager:failedLoadingStoreWithCause:context:wasCloud:, the application should not be using the persistence coordinator. + * You should probably unset your managed object contexts here to prevent exceptions/hangs in your applications (the coordinator is locked and its store removed). + * Also useful for indicating in your user interface that the store is loading. + * + * @param isCloudStore YES if the cloud store will be loaded. + * NO if the local store will be loaded. + */ +@optional +- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager willLoadStoreIsCloud:(BOOL)isCloudStore; + +/** Triggered when the store manager loads a persistence store. + * + * The manager is done handling the attempt to load the store. This is where you'll init/update your application's persistence layer. + * You should probably create your main managed object context here. + * + * Note the coordinator could change during the application's lifetime (you'll get a new -ubiquityStoreManager:didLoadStoreForCoordinator:isCloud: if this happens). + * + * @param isCloudStore YES if the cloud store was just loaded. + * NO if the local store was just loaded. + */ +@required +- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didLoadStoreForCoordinator:(NSPersistentStoreCoordinator *)coordinator + isCloud:(BOOL)isCloudStore; + +/** Triggered when the store manager fails to loads a persistence store. + * + * If wasCloudStore is YES, -ubiquityStoreManager:handleCloudContentCorruptionIsCloud: will also be called. You should handle the + * failure there, or here if you don't plan to. + * If wasCloudStore is NO, the local store may be irreparably broken. You should probably -deleteLocalStore to fix the persistence layer. + * + * @param wasCloudStore YES if the error was caused while attempting to load the cloud store. + * NO if the error was caused while attempting to load the local store. + */ +@optional +- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager failedLoadingStoreWithCause:(UbiquityStoreErrorCause)cause + context:(id)context wasCloud:(BOOL)wasCloudStore; + +/** Triggered when the store manager has detected that the cloud content has failed to import on one of the devices. + * + * TL;DR: The recommended way to implement this method is to return NO (so the default solution will be effected). + * If storeHealthy is YES, you can show the user that iCloud is being fixed. + * If storeHealthy is NO, you should tell the user this device is waiting and he should open the app on his other device(s) so they can + * attempt to fix the situation. + * + * Why did this happen? + * + * When cloud content (transaction logs) fail to import into the cloud store on this device, the result is that the cloud store is no + * longer guaranteed to be the same as the cloud store on other devices. Moreover, there is no more guarantee that changes made to the + * cloud store will sync to other devices. iCloud sync for the cloud store is therefore effectively broken. + * + * When this happens, there is only one recovery: The cloud store must be recreated from scratch. + * + * Unfortunately, this situation tends to occur very easily because of an Apple bug with regards to synchronizing Core Data relationships: + * When two devices simultaneously modify a relationship, the resulting transaction logs can cause an irreparable conflict. + * + * You can implement this method to be notified of when this situation occurs. If you plan to handle the problem yourself and deal with + * the corruption, return YES to disable the manager's default strategy. + * If you want the manager to effect its default solution, return NO (or don't implement this method). + * + * The default solution to this problem is to unload the cloud store on all devices where transaction logs can no longer be imported into + * the store. A device that has not noticed any import problems will be notified of cloud corruption in other devices and initiate a + * rebuild of the cloud content. + * + * If you want to handle the corruption yourself, you have a few options. Keep in mind: To fix the situation you will need to create + * a new cloud store; only a new cloud store can guarantee that all devices are back in-sync. Here's what you could do: + * - Switch to the local store (manager.cloudEnabled = NO). + * NOTE: The cloud data and cloud syncing will be unavailable. + * - Delete the cloud data and recreate it by seeding it with the local store ([manager deleteCloudStoreLocalOnly:NO]). + * NOTE: The existing cloud data will be lost. + * - Make the existing cloud data local and disable iCloud ([manager migrateCloudToLocalAndDeleteCloudStoreLocalOnly:NO]). + * NOTE: The existing local store will be lost. + * NOTE: The cloud data known by this device will become available again. + * NOTE: If you set localOnly to NO, the user can re-enable iCloud but any cloud data not synced to this device will be lost. + * - Rebuild the cloud content by seeding it with the cloud store of this device ([manager rebuildCloudContentFromCloudStoreOrLocalStore:YES]). + * NOTE: iCloud functionality will be completely restored with the cloud data known by this device. + * NOTE: Any cloud changes on other devices that failed to sync to this device will be lost. + * NOTE: If you specify YES for allowRebuildFromLocalStore and the cloud store on this device is unusable for repairing the cloud + * content, a new cloud store will be created from the local store instead. + * + * Keep in mind that if storeHealthy is YES, the cloud store will, if enabled, still be loaded. If storeHealthy is NO, the cloud store + * will, if enabled, have been unloaded before this method is called and no store will be available at this point. + * + * @param storeHealthy YES if this device has no loading or syncing problems with the cloud store. + * NO if this device can no longer open or sync with the cloud store. + * @return YES if you've handled the corruption yourself and want to disable the manager's default strategy for resolving corruption. + * NO if you just use this method to inform the user or your application and want the manager to handle the problem for you. + */ +@optional +- (BOOL)ubiquityStoreManager:(UbiquityStoreManager *)manager handleCloudContentCorruptionWithHealthyStore:(BOOL)storeHealthy; + +/** Triggered when the cloud content is deleted. + * + * When the cloud store is deleted, it may be that the user has deleted his cloud data for the app from one of his devices. + * It is therefore not necessarily desirable to immediately re-create a cloud store. By default, the manager will just unload the store, + * leaving you with no persistence. + * + * It may be desirable to show UI to the user allowing him to choose between re-enabling iCloud ([manager deleteCloudStoreLocalOnly:NO]) + * or disabling it and switching back to local data (manager.cloudEnabled = NO). + */ +@optional +- (void)ubiquityStoreManagerHandleCloudContentDeletion:(UbiquityStoreManager *)manager; + +/** Triggered when the store manager encounters an error. Mainly useful to handle error conditions/logging in whatever way you see fit. + * + * If you don't implement this method, the manager will instead detail the error in a few log statements. + */ +@optional +- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didEncounterError:(NSError *)error + cause:(UbiquityStoreErrorCause)cause context:(id)context; + +/** Triggered whenever the store manager has information to share about its operation. Mainly useful to plug in your own logger. + * + * If you don't implement this method, the manager will just log the message using NSLog. + */ +@optional +- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager log:(NSString *)message; + +/** Triggered when the store manager needs to perform a manual store migration. + * + * Implementing this method is required if you set -migrationStrategy to UbiquityStoreMigrationStrategyManual. + * + * @param error If the migration fails, write out an error object that describes the problem. + * @return YES when the migration was successful and the new store may be loaded. + * NO to error out and not load the new store (new store will be cleaned up if it exists). + */ +@optional +- (BOOL)ubiquityStoreManager:(UbiquityStoreManager *)manager + manuallyMigrateStore:(NSURL *)oldStore withOptions:oldStoreOptions + toStore:(NSURL *)newStore withOptions:newStoreOptions error:(NSError **)error; + +@end + +@interface UbiquityStoreManager : NSObject + +/** The delegate provides the managed object context to use and is informed of events in the ubiquity manager. */ +@property(nonatomic, weak) id delegate; + +/** Determines what strategy to use when migrating from one store to another (eg. local -> cloud). Default is UbiquityStoreMigrationStrategyCopyEntities. */ +@property(nonatomic, assign) UbiquityStoreMigrationStrategy migrationStrategy; + +/** Indicates whether the iCloud store or the local store is in use. */ +@property(nonatomic) BOOL cloudEnabled; + +/** Start managing an optionally ubiquitous store coordinator. + * @param contentName The name of the local and cloud stores that this manager will create. If nil, "UbiquityStore" will be used. + * @param model The managed object model the store should use. If nil, all the main bundle's models will be merged. + * @param localStoreURL The location where the non-ubiquitous (local) store should be kept. If nil, the local store will be put in the application support directory. + * @param containerIdentifier The identifier of the ubiquity container to use for the ubiquitous store. If nil, the entitlement's primary container identifier will be used. + * @param additionalStoreOptions Additional persistence options that the stores should be initialized with. + * @param delegate The application controller that will be handling the application's persistence responsibilities. + */ +- (id)initStoreNamed:(NSString *)contentName withManagedObjectModel:(NSManagedObjectModel *)model localStoreURL:(NSURL *)localStoreURL + containerIdentifier:(NSString *)containerIdentifier additionalStoreOptions:(NSDictionary *)additionalStoreOptions + delegate:(id)delegate; + +#pragma mark - Store Management + +/** + * Clear and re-open the store. + * + * This is rarely useful if you want to re-try opening the active store. You usually won't need to invoke this manually. + */ +- (void)reloadStore; + +/** + * This will delete all the data from iCloud for this application. + * + * @param localOnly If YES, the iCloud data will be redownloaded when needed. + * If NO, the container's data will be permanently lost. + * + * Unless you intend to delete more than just the active cloud store, you should probably use -deleteCloudStoreLocalOnly: instead. + */ +- (void)deleteCloudContainerLocalOnly:(BOOL)localOnly; + +/** + * This will delete the iCloud store. + * + * @param localOnly If YES, the iCloud transaction logs will be redownloaded and the store rebuilt. + * If NO, the store will be permanently lost and a new one will be created by migrating the device's local store. + */ +- (void)deleteCloudStoreLocalOnly:(BOOL)localOnly; + +/** + * This will delete the local store. + */ +- (void)deleteLocalStore; + +/** + * This will delete the local store and migrate the cloud store to a new local store. The cloud store is subsequently deleted. The device will subsequently load the new local store (disable cloud). + * + * @param localOnly If YES, the cloud content is not deleted from iCloud. + * If NO, the cloud store will be permanently lost and a new one will be created by migrating the new local store when iCloud is re-enabled. + */ +- (void)migrateCloudToLocalAndDeleteCloudStoreLocalOnly:(BOOL)localOnly; + +/** + * This will delete the cloud content and recreate a new cloud store by seeding it with the current cloud store. + * Any cloud content and cloud store changes on other devices that are not present on this device's cloud store will be lost. + * + * @param allowRebuildFromLocalStore If YES and the cloud content cannot be rebuilt from the cloud store, the local store will be used + * instead. Beware: All former cloud content will be lost. + */ +- (void)rebuildCloudContentFromCloudStoreOrLocalStore:(BOOL)allowRebuildFromLocalStore; + +#pragma mark - Store Information + +/** + * Determine whether it's safe to seed the cloud store with a local store. + */ +- (BOOL)cloudSafeForSeeding; + +/** + * @return URL to the active app's ubiquity container. + */ +- (NSURL *)URLForCloudContainer; + +/** + * @return URL to the directory where we put cloud store databases for this app. + */ +- (NSURL *)URLForCloudStoreDirectory; + +/** + * @return URL to the active cloud store's database. + */ +- (NSURL *)URLForCloudStore; + +/** + * @return URL to the directory where we put cloud store transaction logs for this app. + */ +- (NSURL *)URLForCloudContentDirectory; + +/** + * @return URL to the active cloud store's transaction logs. + */ +- (NSURL *)URLForCloudContent; + +/** + * @return URL to the directory where we put the local store database for this app. + */ +- (NSURL *)URLForLocalStoreDirectory; + +/** + * @return URL to the local store's database. + */ +- (NSURL *)URLForLocalStore; + +@end diff --git a/UbiquityStoreManager/UbiquityStoreManager.m b/UbiquityStoreManager/UbiquityStoreManager.m new file mode 100644 index 0000000..22075c2 --- /dev/null +++ b/UbiquityStoreManager/UbiquityStoreManager.m @@ -0,0 +1,1282 @@ +/** + * Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com) + * + * See the enclosed file LICENSE for license information (LGPLv3). If you did + * not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt + * + * @author Maarten Billemont + * @license http://www.gnu.org/licenses/lgpl-3.0.txt + */ + +// +// UbiquityStoreManager.m +// UbiquityStoreManager +// +// Created by Maarten Billemont on 05/11/09. +// + +#import "UbiquityStoreManager.h" +#import "JRSwizzle.h" +#import "NSError+UbiquityStoreManager.h" + + +NSString *const UbiquityManagedStoreDidChangeNotification = @"UbiquityManagedStoreDidChangeNotification"; +NSString *const UbiquityManagedStoreDidImportChangesNotification = @"UbiquityManagedStoreDidImportChangesNotification"; +NSString *const CloudEnabledKey = @"USMCloudEnabledKey"; // local: Whether the user wants the app on this device to use iCloud. +NSString *const StoreUUIDKey = @"USMStoreUUIDKey"; // cloud: The UUID of the active cloud store. +NSString *const StoreContentCorruptedKey = @"USMStoreCorruptedKey"; // cloud: Set to YES when a cloud content corruption has been detected. +NSString *const CloudStoreDirectory = @"CloudStore.nosync"; +NSString *const CloudStoreMigrationSource = @"MigrationSource.sqlite"; +NSString *const CloudContentDirectory = @"CloudLogs"; + +@interface UbiquityStoreManager() + +@property(nonatomic, copy) NSString *contentName; +@property(nonatomic, strong) NSManagedObjectModel *model; +@property(nonatomic, copy) NSURL *localStoreURL; +@property(nonatomic, copy) NSString *containerIdentifier; +@property(nonatomic, copy) NSDictionary *additionalStoreOptions; +@property(nonatomic, readonly) NSString *storeUUID; +@property(nonatomic, strong) NSString *tentativeStoreUUID; +@property(nonatomic, strong) NSOperationQueue *persistentStorageQueue; +@property(nonatomic, readonly) NSPersistentStoreCoordinator *persistentStoreCoordinator; +@property(nonatomic, strong) id currentIdentityToken; +@property(nonatomic, strong) NSURL *migrationStoreURL; +@property(nonatomic) BOOL attemptingCloudRecovery; +@property(nonatomic) NSString *cloudStoreCorruptUUID; +@property(nonatomic) BOOL cloudStoreLoaded; +@end + +@implementation UbiquityStoreManager { + NSPersistentStoreCoordinator *_persistentStoreCoordinator; + NSOperationQueue *_presentedItemOperationQueue; +} + ++ (void)initialize { + + if (![self respondsToSelector:@selector(jr_swizzleMethod:withMethod:error:)]) { + NSLog( @"UbiquityStoreManager: Warning: JRSwizzle not present, won't be able to detect desync issues." ); + return; + } + + NSError *error = nil; + if (![NSError jr_swizzleMethod:@selector(initWithDomain:code:userInfo:) + withMethod:@selector(init_USM_WithDomain:code:userInfo:) + error:&error]) + NSLog( @"UbiquityStoreManager: Warning: Failed to swizzle, won't be able to detect desync issues. Cause: %@", error ); +} + +- (id)initStoreNamed:(NSString *)contentName withManagedObjectModel:(NSManagedObjectModel *)model localStoreURL:(NSURL *)localStoreURL + containerIdentifier:(NSString *)containerIdentifier additionalStoreOptions:(NSDictionary *)additionalStoreOptions + delegate:(id)delegate { + + if (!(self = [super init])) + return nil; + + // Parameters. + _delegate = delegate; + _contentName = contentName == nil? @"UbiquityStore": contentName; + _model = model == nil? [NSManagedObjectModel mergedModelFromBundles:nil]: model; + if (!localStoreURL) + localStoreURL = [[[self URLForApplicationContainer] + URLByAppendingPathComponent:self.contentName isDirectory:NO] + URLByAppendingPathExtension:@"sqlite"]; + _localStoreURL = localStoreURL; + _containerIdentifier = containerIdentifier; + _additionalStoreOptions = additionalStoreOptions == nil? [NSDictionary dictionary]: additionalStoreOptions; + + // Private vars. + _currentIdentityToken = [[NSFileManager defaultManager] ubiquityIdentityToken]; + _migrationStrategy = UbiquityStoreMigrationStrategyCopyEntities; + _persistentStorageQueue = [NSOperationQueue new]; + _persistentStorageQueue.name = [NSString stringWithFormat:@"%@PersistenceQueue", NSStringFromClass( [self class] )]; + _persistentStorageQueue.maxConcurrentOperationCount = 1; + _presentedItemOperationQueue = [NSOperationQueue new]; + _presentedItemOperationQueue.name = [NSString stringWithFormat:@"%@PresenterQueue", NSStringFromClass( [self class] )]; + + // Observe application events. + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyValueStoreChanged:) + name:NSUbiquitousKeyValueStoreDidChangeExternallyNotification + object:[NSUbiquitousKeyValueStore defaultStore]]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(cloudStoreChanged:) + name:NSUbiquityIdentityDidChangeNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(ubiquityStoreManagerDidDetectCorruption:) + name:UbiquityManagedStoreDidDetectCorruptionNotification + object:nil]; +#if TARGET_OS_IPHONE + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidBecomeActive:) + name:UIApplicationDidBecomeActiveNotification + object:nil]; +// [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillEnterForeground:) +// name:UIApplicationWillEnterForegroundNotification +// object:nil]; +// [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidEnterBackground:) +// name:UIApplicationDidEnterBackgroundNotification +// object:nil]; +// [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillTerminate:) +// name:UIApplicationWillTerminateNotification +// object:nil]; +#else + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidBecomeActive:) + name:NSApplicationDidBecomeActiveNotification + object:nil]; +#endif + + [self reloadStore]; + + return self; +} + +- (void)dealloc { + + [NSFileCoordinator removeFilePresenter:self]; + [self.persistentStorageQueue addOperations:@[ + [NSBlockOperation blockOperationWithBlock:^{ + [self.persistentStoreCoordinator tryLock]; + [self clearStore]; + [self.persistentStoreCoordinator unlock]; + }] + ] waitUntilFinished:YES]; +} + +#pragma mark - File Handling + +- (NSURL *)URLForApplicationContainer { + + NSURL *applicationSupportURL = [[[NSFileManager defaultManager] URLsForDirectory:NSApplicationSupportDirectory + inDomains:NSUserDomainMask] lastObject]; + +#if TARGET_OS_IPHONE + // On iOS, each app is in a sandbox so we don't need to app-scope this directory. + return applicationSupportURL; +#else + // The directory is shared between all apps on the system so we need to scope it for the running app. + applicationSupportURL = [applicationSupportURL URLByAppendingPathComponent:[NSRunningApplication currentApplication].bundleIdentifier isDirectory:YES]; + + NSError *error = nil; + if (![[NSFileManager defaultManager] createDirectoryAtURL:applicationSupportURL + withIntermediateDirectories:YES attributes:nil error:&error]) + [self error:error cause:UbiquityStoreErrorCauseCreateStorePath context:applicationSupportURL.path]; + + return applicationSupportURL; +#endif +} + +- (NSURL *)URLForCloudContainer { + + return [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:self.containerIdentifier]; +} + +- (NSURL *)URLForCloudStoreDirectory { + + // We put the database in the ubiquity container with a .nosync extension (must not be synced by iCloud), + // so that its presence is tied closely to whether iCloud is enabled or not on the device + // and the user can delete the store by deleting his iCloud data for the app from Settings. + return [[self URLForCloudContainer] URLByAppendingPathComponent:CloudStoreDirectory isDirectory:YES]; +} + +- (NSURL *)URLForCloudStore { + + // Our cloud store is in the cloud store databases directory and is identified by the active storeUUID. + return [[[self URLForCloudStoreDirectory] URLByAppendingPathComponent:self.storeUUID isDirectory:NO] + URLByAppendingPathExtension:@"sqlite"]; +} + +- (NSURL *)URLForCloudContentDirectory { + + // The transaction logs are in the ubiquity container and are synced by iCloud. + return [[self URLForCloudContainer] URLByAppendingPathComponent:CloudContentDirectory isDirectory:YES]; +} + +- (NSURL *)URLForCloudContent { + + // Our cloud store's logs are in the cloud store transaction logs directory and is identified by the active storeUUID. + return [[self URLForCloudContentDirectory] URLByAppendingPathComponent:self.storeUUID isDirectory:YES]; +} + +- (NSURL *)URLForLocalStoreDirectory { + + return [self.localStoreURL URLByDeletingLastPathComponent]; +} + +- (NSURL *)URLForLocalStore { + + return self.localStoreURL; +} + + +#pragma mark - Utilities + +- (void)log:(NSString *)format, ... NS_FORMAT_FUNCTION(1, 2) { + + va_list argList; + va_start(argList, format); + NSString *message = [[NSString alloc] initWithFormat:format arguments:argList]; + va_end(argList); + + if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:log:)]) + [self.delegate ubiquityStoreManager:self log:message]; + else + NSLog( @"UbiquityStoreManager: %@", message ); +} + +- (void)error:(NSError *)error cause:(UbiquityStoreErrorCause)cause context:(id)context { + + if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didEncounterError:cause:context:)]) + [self.delegate ubiquityStoreManager:self didEncounterError:error cause:cause context:context]; + else { + [self log:@"Error (cause:%u): %@", cause, error]; + + if (context) + [self log:@" - Context : %@", context]; + NSError *underlyingError = [[error userInfo] objectForKey:NSUnderlyingErrorKey]; + if (underlyingError) + [self log:@" - Underlying: %@", underlyingError]; + NSArray *detailedErrors = [[error userInfo] objectForKey:NSDetailedErrorsKey]; + for (NSError *detailedError in detailedErrors) + [self log:@" - Detail : %@", detailedError]; + } +} + +#pragma mark - Store Management + +- (NSPersistentStoreCoordinator *)persistentStoreCoordinator { + + NSAssert([NSOperationQueue currentQueue] == self.persistentStorageQueue, + @"Persistence coordinator should only be accessed from the persistence queue."); + + if (!_persistentStoreCoordinator) { + _persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.model]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(mergeChanges:) + name:NSPersistentStoreDidImportUbiquitousContentChangesNotification + object:_persistentStoreCoordinator]; + } + + return _persistentStoreCoordinator; +} + +- (void)resetPersistentStoreCoordinator { + + NSAssert([NSOperationQueue currentQueue] == self.persistentStorageQueue, + @"Persistence coordinator should only be modified from the persistence queue."); + + BOOL wasLocked = NO; + if (_persistentStoreCoordinator) { + wasLocked = ![_persistentStoreCoordinator tryLock]; + [_persistentStoreCoordinator unlock]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:nil object:_persistentStoreCoordinator]; + } + + if (wasLocked) + [self.persistentStoreCoordinator lock]; +} + +- (void)clearStore { + + NSAssert([NSOperationQueue currentQueue] == self.persistentStorageQueue, + @"Store should only be cleared from the persistence queue."); + + [self log:@"Clearing stores..."]; + if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:willLoadStoreIsCloud:)]) + [self.delegate ubiquityStoreManager:self willLoadStoreIsCloud:self.cloudEnabled]; + + // Remove the store from the coordinator. + self.cloudStoreLoaded = NO; + [NSFileCoordinator removeFilePresenter:self]; + NSError *error = nil; + for (NSPersistentStore *store in self.persistentStoreCoordinator.persistentStores) + if (![self.persistentStoreCoordinator removePersistentStore:store error:&error]) + [self error:error cause:UbiquityStoreErrorCauseClearStore context:store]; + + if ([self.persistentStoreCoordinator.persistentStores count]) + // We couldn't remove all the stores, make a new PSC instead. + [self resetPersistentStoreCoordinator]; + + dispatch_async( dispatch_get_main_queue(), ^{ + [[NSNotificationCenter defaultCenter] postNotificationName:UbiquityManagedStoreDidChangeNotification + object:self userInfo:nil]; + } ); +} + +- (void)reloadStore { + + [self log:@"(Re)loading store..."]; + if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:willLoadStoreIsCloud:)]) + [self.delegate ubiquityStoreManager:self willLoadStoreIsCloud:self.cloudEnabled]; + + [self.persistentStorageQueue addOperationWithBlock:^{ + [self.persistentStoreCoordinator lock]; + @try { + if (self.cloudEnabled) + [self loadCloudStore]; + else + [self loadLocalStore]; + } + @finally { + [self.persistentStoreCoordinator unlock]; + } + }]; +} + +- (void)loadCloudStore { + + NSAssert([NSOperationQueue currentQueue] == self.persistentStorageQueue, + @"Active store should only be changed from the persistence queue."); + + [self log:@"Will load cloud store: %@ (%@).", self.storeUUID, _tentativeStoreUUID? @"tentative": @"definite"]; + + // Mark store as healthy: opening the store now will tell us whether it's still corrupt. + self.cloudStoreLoaded = NO; + + id context = nil; + NSError *error = nil; + UbiquityStoreErrorCause cause = UbiquityStoreErrorCauseNoError; + @try { + [self clearStore]; + + // Check if the user is logged into iCloud on the device. + if (![self URLForCloudContainer]) { + cause = UbiquityStoreErrorCauseNoAccount; + return; + } + + // Create the path to the cloud store and content if it doesn't exist yet. + NSURL *cloudStoreURL = [self URLForCloudStore]; + NSURL *cloudStoreContentURL = [self URLForCloudContent]; + NSURL *cloudStoreDirectoryURL = [self URLForCloudStoreDirectory]; + if (![[NSFileManager defaultManager] createDirectoryAtPath:cloudStoreDirectoryURL.path + withIntermediateDirectories:YES attributes:nil error:&error]) + [self error:error cause:cause = UbiquityStoreErrorCauseCreateStorePath context:context = cloudStoreDirectoryURL.path]; + if (![[NSFileManager defaultManager] createDirectoryAtPath:cloudStoreContentURL.path + withIntermediateDirectories:YES attributes:nil error:&error]) + [self error:error cause:cause = UbiquityStoreErrorCauseCreateStorePath context:context = cloudStoreContentURL.path]; + + // Clean up the cloud store if the cloud content got deleted. + BOOL storeExists = [[NSFileManager defaultManager] fileExistsAtPath:cloudStoreURL.path]; + BOOL storeContentExists = [[NSFileManager defaultManager] startDownloadingUbiquitousItemAtURL:cloudStoreContentURL error:nil]; + if (storeExists && !storeContentExists) { + // We have a cloud store but no cloud content. The cloud content was deleted: + // The existing store cannot sync anymore and needs to be recreated. + [self log:@"Deleting cloud store: it has no cloud content."]; + [self removeItemAtURL:cloudStoreURL localOnly:NO]; + } + + // Check if we need to seed the store by migrating another store into it. + UbiquityStoreMigrationStrategy migrationStrategy = self.migrationStrategy; + NSURL *migrationStoreURL = self.migrationStoreURL? self.migrationStoreURL: [self localStoreURL]; + if (![self cloudSafeForSeeding] || ![[NSFileManager defaultManager] fileExistsAtPath:migrationStoreURL.path]) + migrationStrategy = UbiquityStoreMigrationStrategyNone; + + // Load the cloud store. + NSMutableDictionary *cloudStoreOptions = [@{ + NSPersistentStoreUbiquitousContentNameKey : self.contentName, + NSPersistentStoreUbiquitousContentURLKey : cloudStoreContentURL, + NSMigratePersistentStoresAutomaticallyOption : @YES, + NSInferMappingModelAutomaticallyOption : @YES, + } mutableCopy]; + NSMutableDictionary *migrationStoreOptions = [@{ + NSReadOnlyPersistentStoreOption : @YES, + } mutableCopy]; + [cloudStoreOptions addEntriesFromDictionary:self.additionalStoreOptions]; + [migrationStoreOptions addEntriesFromDictionary:self.additionalStoreOptions]; + [self loadStoreAtURL:cloudStoreURL withOptions:cloudStoreOptions + migratingStoreAtURL:migrationStoreURL withOptions:migrationStoreOptions usingStrategy:migrationStrategy + cause:&cause context:&context]; + } + @finally { + BOOL wasExplicitMigration = self.migrationStoreURL != nil; + self.migrationStoreURL = nil; + + if (cause == UbiquityStoreErrorCauseNoError) { + // Store loaded successfully. + [self confirmTentativeStoreUUID]; + self.cloudStoreLoaded = YES; + self.attemptingCloudRecovery = NO; + [NSFileCoordinator addFilePresenter:self]; + + [self log:@"Cloud enabled and successfully loaded cloud store."]; + + // Give it some "time" to import any incoming transaction logs. This is important: + // 1. To see if this store is a healthy candidate for content corruption rebuild. + // 2. To make sure our store is up-to-date before we destroy the cloud content and rebuild it from the store. + dispatch_after( dispatch_time( DISPATCH_TIME_NOW, NSEC_PER_SEC * 30 ), + dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0 ), ^{ + [self.persistentStorageQueue addOperationWithBlock:^{ + if (![self.cloudStoreCorruptUUID isEqualToString:self.storeUUID]) + [self handleCloudContentCorruption]; + }]; + } ); + } + else { + // An error occurred in the @try block. + [self unsetTentativeStoreUUID]; + [self clearStore]; + + // If we were performing explicit migration, try without in case the problem was caused by the migration store. + if (wasExplicitMigration) { + [self log:@"Cloud enabled but failed to load cloud store. Was performing explicit migration; will try without. (cause:%u, %@)", + cause, context]; + [self reloadStore]; + return; + } + + // If we haven't attempted recovery yet (ie. delete the local store), try that first. + if (!self.attemptingCloudRecovery) { + [self log:@"Cloud enabled but failed to load cloud store. Attempting recovery by rebuilding from cloud content. (cause:%u, %@)", + cause, context]; + self.attemptingCloudRecovery = YES; + [self deleteCloudStoreLocalOnly:YES]; + return; + } + self.attemptingCloudRecovery = NO; + + // Failed to load regardless of recovery attempt. Mark store as corrupt. + [self log:@"Cloud enabled but failed to load cloud store. Marking cloud store as corrupt. Store will be unavailable. (cause:%u, %@)", + cause, context]; + [self markCloudStoreCorrupted]; + } + + NSPersistentStoreCoordinator *psc = self.persistentStoreCoordinator; + dispatch_async( dispatch_get_main_queue(), ^{ + if (cause == UbiquityStoreErrorCauseNoError) { + // Store loaded successfully. + if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didLoadStoreForCoordinator:isCloud:)]) + [self.delegate ubiquityStoreManager:self didLoadStoreForCoordinator:psc isCloud:YES]; + + [[NSNotificationCenter defaultCenter] postNotificationName:UbiquityManagedStoreDidChangeNotification + object:self userInfo:nil]; + } + else { + // Store failed to load, inform delegate. + if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:failedLoadingStoreWithCause:context:wasCloud:)]) + [self.delegate ubiquityStoreManager:self failedLoadingStoreWithCause:cause context:context wasCloud:YES]; + } + } ); + } +} + +- (void)loadLocalStore { + + NSAssert([NSOperationQueue currentQueue] == self.persistentStorageQueue, + @"Active store should only be changed from the persistence queue."); + + [self log:@"Will load local store."]; + + id context = nil; + NSError *error = nil; + UbiquityStoreErrorCause cause = UbiquityStoreErrorCauseNoError; + @try { + [self clearStore]; + + // Make sure local store directory exists. + NSURL *localStoreURL = [self URLForLocalStore]; + NSURL *localStoreDirectoryURL = [self URLForLocalStoreDirectory]; + if (![[NSFileManager defaultManager] createDirectoryAtPath:localStoreDirectoryURL.path + withIntermediateDirectories:YES attributes:nil error:&error]) { + [self error:error cause:cause = UbiquityStoreErrorCauseCreateStorePath context:context = localStoreDirectoryURL.path]; + return; + } + + // If the local store doesn't exist yet and a migrationStore is set, copy it. + // Check if we need to seed the store by migrating another store into it. + UbiquityStoreMigrationStrategy migrationStrategy = self.migrationStrategy; + NSURL *migrationStoreURL = self.migrationStoreURL; + if (![[NSFileManager defaultManager] fileExistsAtPath:self.migrationStoreURL.path] || + [[NSFileManager defaultManager] fileExistsAtPath:localStoreURL.path]) + migrationStrategy = UbiquityStoreMigrationStrategyNone; + + // Load the local store. + NSMutableDictionary *localStoreOptions = [@{ + NSMigratePersistentStoresAutomaticallyOption : @YES, + NSInferMappingModelAutomaticallyOption : @YES + } mutableCopy]; + NSMutableDictionary *migrationStoreOptions = [@{ + NSReadOnlyPersistentStoreOption : @YES + } mutableCopy]; + if ([[self.migrationStoreURL URLByDeletingLastPathComponent].path + isEqualToString:[self URLForCloudStoreDirectory].path]) + // Migration store is a cloud store. + [migrationStoreOptions addEntriesFromDictionary:@{ + NSPersistentStoreUbiquitousContentNameKey : self.contentName, + NSPersistentStoreUbiquitousContentURLKey : [self URLForCloudContent], + }]; + [localStoreOptions addEntriesFromDictionary:self.additionalStoreOptions]; + [migrationStoreOptions addEntriesFromDictionary:self.additionalStoreOptions]; + [self loadStoreAtURL:localStoreURL withOptions:localStoreOptions + migratingStoreAtURL:migrationStoreURL withOptions:migrationStoreOptions usingStrategy:migrationStrategy + cause:&cause context:&context]; + } + @finally { + BOOL wasExplicitMigration = self.migrationStoreURL != nil; + self.migrationStoreURL = nil; + + if (cause == UbiquityStoreErrorCauseNoError) { + // Store loaded successfully. + [NSFileCoordinator addFilePresenter:self]; + + [self log:@"Cloud disabled and successfully loaded local store."]; + } + else { + // An error occurred in the @try block. + [self clearStore]; + + // If we were performing explicit migration, try without in case the problem was caused by the migration store. + if (wasExplicitMigration) { + [self log:@"Cloud disabled but failed to load local store. Was performing explicit migration; will try without. (cause:%u, %@)", + cause, context]; + [self reloadStore]; + return; + } + + [self log:@"Cloud disabled but failed to load local store. Store will be unavailable. (cause:%u, %@)", cause, context]; + } + + NSPersistentStoreCoordinator *psc = self.persistentStoreCoordinator; + dispatch_async( dispatch_get_main_queue(), ^{ + if (cause == UbiquityStoreErrorCauseNoError) { + // Store loaded successfully. + if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didLoadStoreForCoordinator:isCloud:)]) { + [self.delegate ubiquityStoreManager:self didLoadStoreForCoordinator:psc isCloud:NO]; + } + + [[NSNotificationCenter defaultCenter] postNotificationName:UbiquityManagedStoreDidChangeNotification + object:self userInfo:nil]; + } + else if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:failedLoadingStoreWithCause:context:wasCloud:)]) + // Store failed to load, inform delegate. + [self.delegate ubiquityStoreManager:self failedLoadingStoreWithCause:cause context:context wasCloud:NO]; + } ); + } +} + +- (void)loadStoreAtURL:(NSURL *)targetStoreURL withOptions:(NSMutableDictionary *)targetStoreOptions + migratingStoreAtURL:(NSURL *)migrationStoreURL withOptions:(NSMutableDictionary *)migrationStoreOptions + usingStrategy:(UbiquityStoreMigrationStrategy)migrationStrategy + cause:(UbiquityStoreErrorCause *)cause context:(id *)context { + + NSAssert([NSOperationQueue currentQueue] == self.persistentStorageQueue, + @"Active store should only be changed from the persistence queue."); + + NSError *error = nil; + __block NSError *error_ = nil; + @try { + switch (migrationStrategy) { + case UbiquityStoreMigrationStrategyCopyEntities: { + [self log:@"Seeding store using strategy: UbiquityStoreMigrationStrategyCopyEntities"]; + NSAssert(migrationStoreURL, @"Cannot migrate: No migration store specified."); + + // Open migration and target store. + NSPersistentStoreCoordinator *migrationCoordinator = + [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.model]; + __block NSPersistentStore *migrationStore = nil; + [[[NSFileCoordinator alloc] initWithFilePresenter:nil] coordinateReadingItemAtURL:migrationStoreURL + options:(NSFileCoordinatorReadingOptions)0 + error:&error byAccessor:^(NSURL *newURL) { + migrationStore = [migrationCoordinator addPersistentStoreWithType:NSSQLiteStoreType + configuration:nil URL:newURL + options:migrationStoreOptions + error:&error_]; + }]; + if (!migrationStore) { + [self error:error_? error_: error cause:*cause = UbiquityStoreErrorCauseOpenSeedStore + context:*context = migrationStoreURL.path]; + break; + } + + NSPersistentStoreCoordinator *targetCoordinator = + [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.model]; + __block NSPersistentStore *targetStore = nil; + [[[NSFileCoordinator alloc] initWithFilePresenter:nil] coordinateReadingItemAtURL:targetStoreURL + options:(NSFileCoordinatorReadingOptions)0 + error:&error byAccessor:^(NSURL *newURL) { + targetStore = [targetCoordinator addPersistentStoreWithType:NSSQLiteStoreType + configuration:nil URL:newURL + options:targetStoreOptions + error:&error_]; + }]; + if (!targetStore) { + [self error:error_? error_: error cause:*cause = UbiquityStoreErrorCauseOpenActiveStore + context:*context = targetStoreURL.path]; + break; + } + + // Set up contexts for them. + NSManagedObjectContext *migrationContext = [NSManagedObjectContext new]; + NSManagedObjectContext *targetContext = [NSManagedObjectContext new]; + migrationContext.persistentStoreCoordinator = migrationCoordinator; + targetContext.persistentStoreCoordinator = targetCoordinator; + + // Migrate metadata. + NSMutableDictionary *metadata = [[migrationCoordinator metadataForPersistentStore:migrationStore] mutableCopy]; + for (NSString *key in [[metadata allKeys] copy]) + if ([key hasPrefix:@"com.apple.coredata.ubiquity"]) + // Don't migrate ubiquitous metadata. + [metadata removeObjectForKey:key]; + [metadata addEntriesFromDictionary:[targetCoordinator metadataForPersistentStore:targetStore]]; + [targetCoordinator setMetadata:metadata forPersistentStore:targetStore]; + + // Migrate entities. + BOOL migrationFailure = NO; + NSMutableDictionary *migratedIDsBySourceID = [[NSMutableDictionary alloc] initWithCapacity:500]; + for (NSEntityDescription *entity in self.model.entities) { + NSFetchRequest *fetch = [NSFetchRequest new]; + fetch.entity = entity; + fetch.fetchBatchSize = 500; + fetch.relationshipKeyPathsForPrefetching = entity.relationshipsByName.allKeys; + + NSArray *localObjects = [migrationContext executeFetchRequest:fetch error:&error]; + if (!localObjects) { + migrationFailure = YES; + break; + } + + for (NSManagedObject *localObject in localObjects) + [self copyMigrateObject:localObject toContext:targetContext usingMigrationCache:migratedIDsBySourceID]; + } + + // Save migrated entities and unload the stores. + if (!migrationFailure && ![targetContext save:&error]) + migrationFailure = YES; + if (![migrationCoordinator removePersistentStore:migrationStore error:&error_]) + [self error:error_ cause:*cause = UbiquityStoreErrorCauseClearStore context:*context = migrationStore]; + if (![targetCoordinator removePersistentStore:targetStore error:&error_]) + [self error:error_ cause:*cause = UbiquityStoreErrorCauseClearStore context:*context = targetStore]; + + // Handle failure by cleaning up the target store. + if (migrationFailure) { + [self error:error cause:*cause = UbiquityStoreErrorCauseSeedStore context:*context = migrationStoreURL.path]; + [self removeItemAtURL:targetStoreURL localOnly:NO]; + break; + } + + // Migration is finished: load the store. + [self loadStoreAtURL:targetStoreURL withOptions:targetStoreOptions + migratingStoreAtURL:nil withOptions:nil usingStrategy:UbiquityStoreMigrationStrategyNone + cause:cause context:context]; + break; + } + + case UbiquityStoreMigrationStrategyIOS: { + [self log:@"Seeding store using strategy: UbiquityStoreMigrationStrategyIOS"]; + NSAssert(migrationStoreURL, @"Cannot migrate: No migration store specified."); + + [[[NSFileCoordinator alloc] initWithFilePresenter:nil] + coordinateReadingItemAtURL:migrationStoreURL options:(NSFileCoordinatorReadingOptions)0 + writingItemAtURL:targetStoreURL options:NSFileCoordinatorWritingForMerging + error:&error byAccessor: + ^(NSURL *newReadingURL, NSURL *newWritingURL) { + // Add the store to migrate. + NSPersistentStore *migrationStore = + [self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType + configuration:nil URL:newReadingURL + options:migrationStoreOptions + error:&error_]; + if (!migrationStore) + [self error:error_ cause:*cause = UbiquityStoreErrorCauseOpenSeedStore + context:*context = newReadingURL.path]; + + else if (![self.persistentStoreCoordinator migratePersistentStore:migrationStore + toURL:newWritingURL + options:targetStoreOptions + withType:NSSQLiteStoreType + error:&error_]) + [self error:error_ cause:*cause = UbiquityStoreErrorCauseSeedStore context:*context = newWritingURL.path]; + else + *cause = UbiquityStoreErrorCauseNoError; + }]; + if (error) + [self error:error cause:UbiquityStoreErrorCauseOpenSeedStore context:migrationStoreURL.path]; + break; + } + + case UbiquityStoreMigrationStrategyManual: { + [self log:@"Seeding store using strategy: UbiquityStoreMigrationStrategyManual"]; + NSAssert(migrationStoreURL, @"Cannot migrate: No migration store specified."); + + // Instruct the delegate to migrate the migration store to the target store. + if (![self.delegate ubiquityStoreManager:self + manuallyMigrateStore:migrationStoreURL withOptions:migrationStoreOptions + toStore:targetStoreURL withOptions:targetStoreOptions error:&error]) { + // Handle failure by cleaning up the target store. + [self error:error cause:*cause = UbiquityStoreErrorCauseSeedStore context:*context = migrationStoreURL.path]; + [self removeItemAtURL:targetStoreURL localOnly:NO]; + break; + } + + // Migration is finished: load the target store. + [self loadStoreAtURL:targetStoreURL withOptions:targetStoreOptions + migratingStoreAtURL:nil withOptions:nil usingStrategy:UbiquityStoreMigrationStrategyNone + cause:cause context:context]; + break; + } + + case UbiquityStoreMigrationStrategyNone: { + [self log:@"Loading store without seeding."]; + NSAssert([self.persistentStoreCoordinator.persistentStores count] == 0, @"PSC should have no stores before trying to load one."); + + // Load the target store. + [[[NSFileCoordinator alloc] initWithFilePresenter:nil] coordinateReadingItemAtURL:targetStoreURL + options:(NSFileCoordinatorReadingOptions)0 + error:&error byAccessor:^(NSURL *newURL) { + if (![self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType + configuration:nil URL:newURL + options:targetStoreOptions + error:&error_]) + [self error:error_ cause:*cause = UbiquityStoreErrorCauseOpenActiveStore context:*context = newURL.path]; + }]; + + if (error) + [self error:error cause:*cause = UbiquityStoreErrorCauseOpenActiveStore context:*context = targetStoreURL.path]; + if ([self.persistentStoreCoordinator.persistentStores count]) + *cause = UbiquityStoreErrorCauseNoError; + + break; + } + } + } + @catch (id exception) { + NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithCapacity:2]; + if (exception) + [userInfo setObject:[(id)exception description] forKey:NSLocalizedFailureReasonErrorKey]; + if (error_) + [userInfo setObject:error_ forKey:NSUnderlyingErrorKey]; + else if (error) + [userInfo setObject:error forKey:NSUnderlyingErrorKey]; + + [self error:[NSError errorWithDomain:NSCocoaErrorDomain code:0 userInfo:userInfo] + cause:*cause = UbiquityStoreErrorCauseSeedStore context:*context = exception]; + } +} + +- (id)copyMigrateObject:(NSManagedObject *)sourceObject toContext:(NSManagedObjectContext *)destinationContext + usingMigrationCache:(NSMutableDictionary *)migratedIDsBySourceID { + + NSAssert([NSOperationQueue currentQueue] == self.persistentStorageQueue, + @"Migration should only be done from the persistence queue."); + + if (!sourceObject) + return nil; + + NSManagedObjectID *destinationObjectID = [migratedIDsBySourceID objectForKey:sourceObject.objectID]; + if (destinationObjectID) + return [destinationContext objectWithID:destinationObjectID]; + + @autoreleasepool { + // Create migrated object. + NSEntityDescription *entity = sourceObject.entity; + NSManagedObject *destinationObject = [NSEntityDescription insertNewObjectForEntityForName:entity.name + inManagedObjectContext:destinationContext]; + [migratedIDsBySourceID setObject:destinationObject.objectID forKey:sourceObject.objectID]; + + // Set attributes + for (NSString *key in entity.attributesByName.allKeys) + [destinationObject setPrimitiveValue:[sourceObject primitiveValueForKey:key] forKey:key]; + + // Set relationships recursively + for (NSRelationshipDescription *relationDescription in entity.relationshipsByName.allValues) { + NSString *key = relationDescription.name; + id value = nil; + + if (relationDescription.isToMany) { + value = [[destinationObject primitiveValueForKey:key] mutableCopy]; + + for (NSManagedObject *element in [sourceObject primitiveValueForKey:key]) + [(NSMutableArray *)value addObject:[self copyMigrateObject:element toContext:destinationContext + usingMigrationCache:migratedIDsBySourceID]]; + } + else + value = [self copyMigrateObject:[sourceObject primitiveValueForKey:key] toContext:destinationContext + usingMigrationCache:migratedIDsBySourceID]; + + [destinationObject setPrimitiveValue:value forKey:key]; + } + + return destinationObject; + } +} + +- (BOOL)cloudSafeForSeeding { + + NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; + [cloud synchronize]; + + if (!self.tentativeStoreUUID && [cloud objectForKey:StoreUUIDKey]) + // Migration is only safe when the target is a new store (tentative or no StoreUUID). + return NO; + + if ([[NSFileManager defaultManager] fileExistsAtPath:[self URLForCloudStore].path]) + // Migration is only safe when the cloud store does not yet exist. + return NO; + + return YES; +} + +- (void)removeItemAtURL:(NSURL *)directoryURL localOnly:(BOOL)localOnly { + + // The file coordination below fails without an error, when the file at directoryURL doesn't exist. We ignore this. + NSError *error = nil; + [[[NSFileCoordinator alloc] initWithFilePresenter:nil] coordinateWritingItemAtURL:directoryURL + options:NSFileCoordinatorWritingForDeleting + error:&error byAccessor: + ^(NSURL *newURL) { + if (![[NSFileManager defaultManager] fileExistsAtPath:newURL.path]) + return; + + NSError *error_ = nil; + if (localOnly && [[NSFileManager defaultManager] isUbiquitousItemAtURL:newURL]) { + if (![[NSFileManager defaultManager] evictUbiquitousItemAtURL:newURL error:&error_]) + [self error:error_ cause:UbiquityStoreErrorCauseDeleteStore context:newURL.path]; + } + else { + if (![[NSFileManager defaultManager] removeItemAtURL:newURL error:&error_]) + [self error:error_ cause:UbiquityStoreErrorCauseDeleteStore context:newURL.path]; + } + }]; + + if (error) + [self error:error cause:UbiquityStoreErrorCauseDeleteStore context:directoryURL.path]; +} + +- (void)deleteCloudContainerLocalOnly:(BOOL)localOnly { + + [self.persistentStorageQueue addOperationWithBlock:^{ + [self log:@"Will delete the cloud container %@.", localOnly? @"on this device": @"on this device and in the cloud"]; + + if (self.cloudEnabled) + [self clearStore]; + + // Delete the whole cloud container. + [self removeItemAtURL:[self URLForCloudContainer] localOnly:localOnly]; + + // Unset the storeUUID so a new one will be created. + if (!localOnly) { + [self createTentativeStoreUUID]; + NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; + [cloud synchronize]; + for (id key in [[cloud dictionaryRepresentation] allKeys]) + [cloud removeObjectForKey:key]; + // Don't synchronize. Otherwise another devices might recreate the cloud store before we do. + } + + if (self.cloudEnabled) + [self reloadStore]; + }]; +} + +- (void)deleteCloudStoreLocalOnly:(BOOL)localOnly { + + [self.persistentStorageQueue addOperationWithBlock:^{ + [self log:@"Will delete the cloud store (UUID:%@) %@.", self.storeUUID, + localOnly? @"on this device": @"on this device and in the cloud"]; + + if (self.cloudEnabled) + [self clearStore]; + + // Clean up any cloud stores and transaction logs. + [self removeItemAtURL:[self URLForCloudStore] localOnly:localOnly]; + [self removeItemAtURL:[self URLForCloudContent] localOnly:localOnly]; + + // Create a tentative StoreUUID so a new cloud store will be created. + if (!localOnly) + [self createTentativeStoreUUID]; + + if (self.cloudEnabled) + [self reloadStore]; + }]; +} + +- (void)deleteLocalStore { + + [self.persistentStorageQueue addOperationWithBlock:^{ + [self log:@"Will delete the local store."]; + + if (!self.cloudEnabled) + [self clearStore]; + + // Remove just the local store. + [self removeItemAtURL:[self URLForLocalStore] localOnly:YES]; + + if (!self.cloudEnabled) + [self reloadStore]; + }]; +} + +- (void)migrateCloudToLocalAndDeleteCloudStoreLocalOnly:(BOOL)localOnly { + + [self.persistentStorageQueue cancelAllOperations]; + [self.persistentStorageQueue addOperationWithBlock:^{ + self.migrationStoreURL = [self URLForCloudStore]; + if (![[NSFileManager defaultManager] fileExistsAtPath:self.migrationStoreURL.path]) { + [self log:@"Cannot migrate cloud to local: Cloud store doesn't exist."]; + self.migrationStoreURL = nil; + return; + } + + [self log:@"Will overwrite the local store with the cloud store."]; + + [self deleteLocalStore]; + self.cloudEnabled = NO; + [self deleteCloudStoreLocalOnly:localOnly]; + }]; +} + +- (void)rebuildCloudContentFromCloudStoreOrLocalStore:(BOOL)allowRebuildFromLocalStore { + + [self.persistentStorageQueue cancelAllOperations]; + [self.persistentStorageQueue addOperationWithBlock:^{ + + NSURL *cloudStoreURL = [self URLForCloudStore]; + if (![[NSFileManager defaultManager] fileExistsAtPath:cloudStoreURL.path]) { + if (allowRebuildFromLocalStore) { + [self log:@"Cannot rebuild cloud content: Cloud store doesn't exist. Will rebuild from local store."]; + [self deleteCloudStoreLocalOnly:NO]; + } + else { + [self log:@"Cannot rebuild cloud content: Cloud store doesn't exist. Giving up."]; + [self reloadStore]; + } + + return; + } + + [self log:@"Will rebuild cloud content from the cloud store."]; + [self clearStore]; + + NSError *error = nil; + __block NSError *error_ = nil; + __block BOOL success = NO; + self.migrationStoreURL = [[self URLForCloudStoreDirectory] URLByAppendingPathComponent:CloudStoreMigrationSource isDirectory:NO]; + [[[NSFileCoordinator alloc] initWithFilePresenter:nil] + coordinateReadingItemAtURL:cloudStoreURL options:(NSFileCoordinatorReadingOptions)0 + writingItemAtURL:self.migrationStoreURL options:NSFileCoordinatorWritingForReplacing + error:&error byAccessor: + ^(NSURL *newReadingURL, NSURL *newWritingURL) { + [[NSFileManager defaultManager] removeItemAtURL:newWritingURL error:&error_]; + success = [[NSFileManager defaultManager] moveItemAtURL:newReadingURL toURL:newWritingURL error :&error_]; + }]; + if (!success) { + [self error:error_? error_: error cause:UbiquityStoreErrorCauseSeedStore context:self.migrationStoreURL.path]; + [self reloadStore]; + return; + } + + [self deleteCloudStoreLocalOnly:NO]; + self.cloudEnabled = YES; + }]; +} + +#pragma mark - Properties + +- (BOOL)cloudEnabled { + + NSUserDefaults *local = [NSUserDefaults standardUserDefaults]; + return [local boolForKey:CloudEnabledKey]; +} + +- (void)setCloudEnabled:(BOOL)enabled { + + if (self.cloudEnabled == enabled) + // No change, do nothing to avoid a needless store reload. + return; + + [self.persistentStorageQueue addOperationWithBlock:^{ + NSUserDefaults *local = [NSUserDefaults standardUserDefaults]; + [local setBool:enabled forKey:CloudEnabledKey]; + + [self reloadStore]; + }]; +} + +- (NSString *)storeUUID { + + if (self.tentativeStoreUUID) + // A tentative StoreUUID is set; this means a new cloud store is being created. + return self.tentativeStoreUUID; + + NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; + [cloud synchronize]; + NSString *storeUUID = [cloud objectForKey:StoreUUIDKey]; + + if (!storeUUID) { + // No StoreUUID is set; this means there is no cloud store yet. Set a new tentative StoreUUID to create one. + if ([NSOperationQueue currentQueue] == self.persistentStorageQueue) + return [self createTentativeStoreUUID]; + else + return @"tentative"; // This is only for -presentedItemURL + } + + return storeUUID; +} + +/** + * When a tentative StoreUUID is set, this operation confirms it and writes it as the new StoreUUID to the iCloud KVS. + */ +- (void)confirmTentativeStoreUUID { + + NSAssert([NSOperationQueue currentQueue] == self.persistentStorageQueue, + @"Tentative StoreUUID should only be confirmed from the persistence queue."); + + if (self.tentativeStoreUUID) { + [self log:@"Confirming tentative StoreUUID: %@", self.tentativeStoreUUID]; + NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; + [cloud setObject:self.tentativeStoreUUID forKey:StoreUUIDKey]; + [cloud removeObjectForKey:StoreContentCorruptedKey]; + [cloud synchronize]; + + [self unsetTentativeStoreUUID]; + } +} + +/** + * Creates a new a tentative StoreUUID. This will result in a new cloud store being created. + */ +- (NSString *)createTentativeStoreUUID { + + NSAssert([NSOperationQueue currentQueue] == self.persistentStorageQueue, + @"Tentative StoreUUID should only be set from the persistence queue."); + + return self.tentativeStoreUUID = [[NSUUID UUID] UUIDString]; +} + +/** + * Creates a new a tentative StoreUUID. This will result in a new cloud store being created. + */ +- (void)unsetTentativeStoreUUID { + + NSAssert([NSOperationQueue currentQueue] == self.persistentStorageQueue, + @"Tentative StoreUUID should only be unset from the persistence queue."); + + self.tentativeStoreUUID = nil; +} + +#pragma mark - NSFilePresenter + +- (NSURL *)presentedItemURL { + + if (self.cloudEnabled) + return [self URLForCloudContent]; + + return [self URLForLocalStore]; +} + +- (NSOperationQueue *)presentedItemOperationQueue { + + return _presentedItemOperationQueue; +} + +- (void)accommodatePresentedItemDeletionWithCompletionHandler:(void (^)(NSError *))completionHandler { + + [self.persistentStorageQueue addOperations:@[ + [NSBlockOperation blockOperationWithBlock:^{ + [self clearStore]; + + if (self.cloudEnabled) { + [self removeItemAtURL:[self URLForCloudStore] localOnly:NO]; + NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; + [cloud removeObjectForKey:StoreContentCorruptedKey]; + [cloud synchronize]; + } + }] + ] waitUntilFinished:YES]; + + completionHandler( nil ); + + if (self.cloudEnabled) { + if ([self.delegate respondsToSelector:@selector(ubiquityStoreManagerHandleCloudContentDeletion:)]) + [self.delegate ubiquityStoreManagerHandleCloudContentDeletion:self]; + } + else + [self reloadStore]; +} + +#pragma mark - Notifications + +- (void)applicationDidBecomeActive:(NSNotification *)note { + + // Check for iCloud identity changes (ie. user logs into another iCloud account). + if (![self.currentIdentityToken isEqual:[[NSFileManager defaultManager] ubiquityIdentityToken]]) + [self cloudStoreChanged:nil]; +} + +//- (void)applicationWillEnterForeground:(NSNotification *)note { +// +// [self reloadStore]; +//} +// +//- (void)applicationDidEnterBackground:(NSNotification *)note { +// +// [self.persistentStorageQueue addOperations:@[ +// [NSBlockOperation blockOperationWithBlock:^{ +// [self clearStore]; +// }] +// ] waitUntilFinished:YES]; +//} +// +//- (void)applicationWillTerminate:(NSNotification *)note { +// +// [self.persistentStorageQueue addOperations:@[ +// [NSBlockOperation blockOperationWithBlock:^{ +// [self clearStore]; +// }] +// ] waitUntilFinished:YES]; +//} + +- (void)keyValueStoreChanged:(NSNotification *)note { + + NSUbiquitousKeyValueStore * cloud = [NSUbiquitousKeyValueStore defaultStore]; + [cloud synchronize]; + + NSArray *changedKeys = (NSArray *)[note.userInfo objectForKey:NSUbiquitousKeyValueStoreChangedKeysKey]; + if ([changedKeys containsObject:StoreUUIDKey]) { + // The UUID of the active store changed. We need to switch to the newly activated store. + [self log:@"StoreUUID changed -> %@ (reason: %@)", + [cloud objectForKey:StoreUUIDKey], [note.userInfo objectForKey:NSUbiquitousKeyValueStoreChangeReasonKey]]; + + [self.persistentStorageQueue cancelAllOperations]; + [self.persistentStorageQueue addOperationWithBlock:^{ + [self unsetTentativeStoreUUID]; + [self cloudStoreChanged:nil]; + }]; + } + + if ([changedKeys containsObject:StoreContentCorruptedKey]) { + // Cloud content corruption was detected or cleared. + [self log:@"StoreContentCorruptedKey changed -> %@ (reason: %@)", + [cloud objectForKey:StoreContentCorruptedKey], [note.userInfo objectForKey:NSUbiquitousKeyValueStoreChangeReasonKey]]; + + if (self.cloudEnabled) + [self.persistentStorageQueue addOperationWithBlock:^{ + if (![self handleCloudContentCorruption] && !self.cloudStoreLoaded) + // Corruption was removed and our cloud store is not yet loaded. Try loading the store again. + [self reloadStore]; + }]; + } +} + +/** + * Triggered when: + * 1. An NSError is created describing a transaction log import failure (UbiquityManagedStoreDidDetectCorruptionNotification). + */ +- (void)ubiquityStoreManagerDidDetectCorruption:(NSNotification *)note { + + [self log:@"Detected iCloud transaction log import failure: %@", note.object]; + [self markCloudStoreCorrupted]; +} + +- (void)markCloudStoreCorrupted { + + self.cloudStoreCorruptUUID = self.storeUUID; + + NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; + [cloud setBool:YES forKey:StoreContentCorruptedKey]; + [cloud synchronize]; + + [self.persistentStorageQueue addOperationWithBlock:^{ + [self handleCloudContentCorruption]; + }]; +} + +- (BOOL)handleCloudContentCorruption { + + NSAssert([NSOperationQueue currentQueue] == self.persistentStorageQueue, + @"Cloud corruption can only be checked from the persistence queue."); + + if (!self.cloudEnabled) + // Cloud not enabled: cannot handle corruption. + return NO; + + NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; + [cloud synchronize]; + + if (![cloud boolForKey:StoreContentCorruptedKey]) + // Cloud content is not corrupt. + return NO; + + // Unload the cloud store if it's loaded and corrupt. + BOOL cloudStoreCorrupt = [self.cloudStoreCorruptUUID isEqualToString:self.storeUUID]; + if (cloudStoreCorrupt) + [self clearStore]; + + // Notify the delegate of corruption. + [self log:@"Cloud content corruption detected (store %@).", cloudStoreCorrupt? @"corrupt": @"healthy"]; + BOOL defaultStrategy = YES; + if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:handleCloudContentCorruptionWithHealthyStore:)]) + defaultStrategy = ![self.delegate ubiquityStoreManager:self + handleCloudContentCorruptionWithHealthyStore:!cloudStoreCorrupt]; + + // Handle corruption. + if (!defaultStrategy) + [self log:@"Application handled cloud corruption."]; + + else { + if (cloudStoreCorrupt) + // Store is corrupt: no store available. + [self log:@"Handling cloud corruption with default strategy: Wait for a remote rebuild."]; + else { + // Store is healthy: rebuild cloud store. + [self log:@"Handling cloud corruption with default strategy: Rebuilding cloud content."]; + [self rebuildCloudContentFromCloudStoreOrLocalStore:NO]; + } + } + + return YES; +} + +/** + * Triggered when: + * 1. Ubiquity identity changed (NSUbiquityIdentityDidChangeNotification). + * 2. Store file was deleted (eg. iCloud container deleted in settings). + * 3. StoreUUID changed (eg. switched to a new cloud store on another device). + */ +- (void)cloudStoreChanged:(NSNotification *)note { + + // Update the identity token in case it changed. + id newIdentityToken = [[NSFileManager defaultManager] ubiquityIdentityToken]; + if (![self.currentIdentityToken isEqual:newIdentityToken]) { + [self log:@"Identity token changed: %@ -> %@", self.currentIdentityToken, newIdentityToken]; + self.currentIdentityToken = newIdentityToken; + } + + // If the cloud store was active, reload it. + if (self.cloudEnabled) + [self reloadStore]; +} + +- (void)mergeChanges:(NSNotification *)note { + + [self.persistentStorageQueue addOperationWithBlock:^{ + NSManagedObjectContext *moc = nil; + if ([self.delegate respondsToSelector:@selector(managedObjectContextForUbiquityChangesInManager:)]) + moc = [self.delegate managedObjectContextForUbiquityChangesInManager:self]; + if (moc) + [self log:@"Importing ubiquity changes into application's MOC. Changes:\n%@", note.userInfo]; + else { + [self log:@"Importing ubiquity changes with default strategy: into persistence store. Changes:\n%@", note.userInfo]; + moc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType]; + moc.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy; + moc.persistentStoreCoordinator = self.persistentStoreCoordinator; + } + + [moc performBlockAndWait:^{ + [moc mergeChangesFromContextDidSaveNotification:note]; + + NSError *error = nil; + if (![moc save:&error]) { + [self error:error cause:UbiquityStoreErrorCauseImportChanges context:note]; + + // Reloading the store will import the changes and make sure that the store hasn't been corrupted. + // TODO: Verify that this works reliably. + [self reloadStore]; + return; + } + + dispatch_async( dispatch_get_main_queue(), ^{ + [[NSNotificationCenter defaultCenter] postNotificationName:UbiquityManagedStoreDidImportChangesNotification + object:self userInfo:[note userInfo]]; + } ); + }]; + }]; +} + +@end diff --git a/iCloudStoreManagerExample.xcodeproj/project.pbxproj b/UbiquityStoreManagerExample.xcodeproj/project.pbxproj similarity index 59% rename from iCloudStoreManagerExample.xcodeproj/project.pbxproj rename to UbiquityStoreManagerExample.xcodeproj/project.pbxproj index 8afe2b2..e731a94 100644 --- a/iCloudStoreManagerExample.xcodeproj/project.pbxproj +++ b/UbiquityStoreManagerExample.xcodeproj/project.pbxproj @@ -20,30 +20,16 @@ 5B4B124815227C6E00153613 /* MasterViewController_iPad.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5B4B124615227C6E00153613 /* MasterViewController_iPad.xib */; }; 5B4B124B15227C6E00153613 /* DetailViewController_iPhone.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5B4B124915227C6E00153613 /* DetailViewController_iPhone.xib */; }; 5B4B124E15227C6E00153613 /* DetailViewController_iPad.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5B4B124C15227C6E00153613 /* DetailViewController_iPad.xib */; }; - 5B4B125615227C6E00153613 /* SenTestingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5B4B125515227C6E00153613 /* SenTestingKit.framework */; }; - 5B4B125715227C6E00153613 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5B4B122615227C6D00153613 /* UIKit.framework */; }; - 5B4B125815227C6E00153613 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5B4B122815227C6D00153613 /* Foundation.framework */; }; - 5B4B125915227C6E00153613 /* CoreData.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5B4B122C15227C6D00153613 /* CoreData.framework */; }; - 5B4B126115227C6E00153613 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5B4B125F15227C6E00153613 /* InfoPlist.strings */; }; - 5B4B126415227C6E00153613 /* iCloudStoreManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 5B4B126315227C6E00153613 /* iCloudStoreManagerTests.m */; }; 5B4B126F15227DFE00153613 /* User.m in Sources */ = {isa = PBXBuildFile; fileRef = 5B4B126E15227DFE00153613 /* User.m */; }; 5B4B127215227DFE00153613 /* Event.m in Sources */ = {isa = PBXBuildFile; fileRef = 5B4B127115227DFE00153613 /* Event.m */; }; - DA79AA3D1558608500BAA07A /* iCloudStoreManagerExample.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = DA79AA3B1558608500BAA07A /* iCloudStoreManagerExample.xcdatamodeld */; }; + 93D3906BB1F143AA15A9BDB9 /* JRSwizzle.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D3982680A3617FB669BBC5 /* JRSwizzle.m */; }; + 93D39247AA48A93AE9BE359A /* NSError+UbiquityStoreManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D39F19900BD420A6E9B72E /* NSError+UbiquityStoreManager.m */; }; + DA79AA3D1558608500BAA07A /* UbiquityStoreManagerExample.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = DA79AA3B1558608500BAA07A /* UbiquityStoreManagerExample.xcdatamodeld */; }; DA79AA411558613F00BAA07A /* UbiquityStoreManager.m in Sources */ = {isa = PBXBuildFile; fileRef = DA79AA401558613F00BAA07A /* UbiquityStoreManager.m */; }; /* End PBXBuildFile section */ -/* Begin PBXContainerItemProxy section */ - 5B4B125A15227C6E00153613 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 5B4B121915227C6D00153613 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 5B4B122115227C6D00153613; - remoteInfo = iCloudStoreManagerExample; - }; -/* End PBXContainerItemProxy section */ - /* Begin PBXFileReference section */ - 5B4B122215227C6D00153613 /* iCloudStoreManagerExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iCloudStoreManagerExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 5B4B122215227C6D00153613 /* UbiquityStoreManagerExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = UbiquityStoreManagerExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 5B4B122615227C6D00153613 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; 5B4B122815227C6D00153613 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 5B4B122A15227C6D00153613 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; @@ -60,20 +46,19 @@ 5B4B124715227C6E00153613 /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/MasterViewController_iPad.xib; sourceTree = ""; }; 5B4B124A15227C6E00153613 /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/DetailViewController_iPhone.xib; sourceTree = ""; }; 5B4B124D15227C6E00153613 /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/DetailViewController_iPad.xib; sourceTree = ""; }; - 5B4B125415227C6E00153613 /* iCloudManagerTests.octest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = iCloudManagerTests.octest; sourceTree = BUILT_PRODUCTS_DIR; }; 5B4B125515227C6E00153613 /* SenTestingKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SenTestingKit.framework; path = Library/Frameworks/SenTestingKit.framework; sourceTree = DEVELOPER_DIR; }; - 5B4B125E15227C6E00153613 /* iCloudStoreManagerTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "iCloudStoreManagerTests-Info.plist"; sourceTree = ""; }; - 5B4B126015227C6E00153613 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; - 5B4B126215227C6E00153613 /* iCloudStoreManagerTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = iCloudStoreManagerTests.h; sourceTree = ""; }; - 5B4B126315227C6E00153613 /* iCloudStoreManagerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = iCloudStoreManagerTests.m; sourceTree = ""; }; 5B4B126D15227DFE00153613 /* User.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = User.h; sourceTree = ""; }; 5B4B126E15227DFE00153613 /* User.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = User.m; sourceTree = ""; }; 5B4B127015227DFE00153613 /* Event.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Event.h; sourceTree = ""; }; 5B4B127115227DFE00153613 /* Event.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Event.m; sourceTree = ""; }; - 5B5E838C152BAB96009C1991 /* iCloudStoreManagerExample.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = iCloudStoreManagerExample.entitlements; sourceTree = ""; }; - DA79AA3115585EBE00BAA07A /* iCloudStoreManagerExample-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "iCloudStoreManagerExample-Info.plist"; sourceTree = ""; }; - DA79AA3215585EBE00BAA07A /* iCloudStoreManagerExample-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "iCloudStoreManagerExample-Prefix.pch"; sourceTree = ""; }; - DA79AA3C1558608500BAA07A /* iCloudStoreManager.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = iCloudStoreManager.xcdatamodel; sourceTree = ""; }; + 5B5E838C152BAB96009C1991 /* UbiquityStoreManagerExample.entitlements */ = {isa = PBXFileReference; lastKnownFileType = file.entitlements; path = UbiquityStoreManagerExample.entitlements; sourceTree = ""; }; + 93D3982680A3617FB669BBC5 /* JRSwizzle.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JRSwizzle.m; sourceTree = ""; }; + 93D39827ACFB3A3A3E1CEF4C /* JRSwizzle.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JRSwizzle.h; sourceTree = ""; }; + 93D39B7366D5AD8A2FEFD7F1 /* NSError+UbiquityStoreManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSError+UbiquityStoreManager.h"; sourceTree = ""; }; + 93D39F19900BD420A6E9B72E /* NSError+UbiquityStoreManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSError+UbiquityStoreManager.m"; sourceTree = ""; }; + DA79AA3115585EBE00BAA07A /* UbiquityStoreManagerExample-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.info; path = "UbiquityStoreManagerExample-Info.plist"; sourceTree = ""; }; + DA79AA3215585EBE00BAA07A /* UbiquityStoreManagerExample-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UbiquityStoreManagerExample-Prefix.pch"; sourceTree = ""; }; + DA79AA3C1558608500BAA07A /* UbiquityStoreManager.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = UbiquityStoreManager.xcdatamodel; sourceTree = ""; }; DA79AA3F1558613F00BAA07A /* UbiquityStoreManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UbiquityStoreManager.h; sourceTree = ""; }; DA79AA401558613F00BAA07A /* UbiquityStoreManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UbiquityStoreManager.m; sourceTree = ""; }; /* End PBXFileReference section */ @@ -90,36 +75,24 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 5B4B125015227C6E00153613 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 5B4B125615227C6E00153613 /* SenTestingKit.framework in Frameworks */, - 5B4B125715227C6E00153613 /* UIKit.framework in Frameworks */, - 5B4B125815227C6E00153613 /* Foundation.framework in Frameworks */, - 5B4B125915227C6E00153613 /* CoreData.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 5B4B121715227C6D00153613 = { isa = PBXGroup; children = ( - DA79AA3E1558613F00BAA07A /* iCloudStoreManager */, - 5B4B122E15227C6D00153613 /* iCloudStoreManagerExample */, - 5B4B125C15227C6E00153613 /* iCloudStoreManagerTests */, + DA79AA3E1558613F00BAA07A /* UbiquityStoreManager */, + 5B4B122E15227C6D00153613 /* UbiquityStoreManagerExample */, 5B4B122515227C6D00153613 /* Frameworks */, 5B4B122315227C6D00153613 /* Products */, + 93D3913AACE1E4369E3900DA /* jrswizzle */, ); sourceTree = ""; }; 5B4B122315227C6D00153613 /* Products */ = { isa = PBXGroup; children = ( - 5B4B122215227C6D00153613 /* iCloudStoreManagerExample.app */, - 5B4B125415227C6E00153613 /* iCloudManagerTests.octest */, + 5B4B122215227C6D00153613 /* UbiquityStoreManagerExample.app */, ); name = Products; sourceTree = ""; @@ -136,11 +109,11 @@ name = Frameworks; sourceTree = ""; }; - 5B4B122E15227C6D00153613 /* iCloudStoreManagerExample */ = { + 5B4B122E15227C6D00153613 /* UbiquityStoreManagerExample */ = { isa = PBXGroup; children = ( - DA79AA3B1558608500BAA07A /* iCloudStoreManagerExample.xcdatamodeld */, - 5B5E838C152BAB96009C1991 /* iCloudStoreManagerExample.entitlements */, + DA79AA3B1558608500BAA07A /* UbiquityStoreManagerExample.xcdatamodeld */, + 5B5E838C152BAB96009C1991 /* UbiquityStoreManagerExample.entitlements */, 5B4B123715227C6E00153613 /* AppDelegate.h */, 5B4B123815227C6E00153613 /* AppDelegate.m */, 5B4B123D15227C6E00153613 /* MasterViewController.h */, @@ -157,54 +130,46 @@ 5B4B126E15227DFE00153613 /* User.m */, 5B4B122F15227C6E00153613 /* Supporting Files */, ); - path = iCloudStoreManagerExample; + path = UbiquityStoreManagerExample; sourceTree = ""; }; 5B4B122F15227C6E00153613 /* Supporting Files */ = { isa = PBXGroup; children = ( - DA79AA3115585EBE00BAA07A /* iCloudStoreManagerExample-Info.plist */, - DA79AA3215585EBE00BAA07A /* iCloudStoreManagerExample-Prefix.pch */, + DA79AA3115585EBE00BAA07A /* UbiquityStoreManagerExample-Info.plist */, + DA79AA3215585EBE00BAA07A /* UbiquityStoreManagerExample-Prefix.pch */, 5B4B123115227C6E00153613 /* InfoPlist.strings */, 5B4B123415227C6E00153613 /* main.m */, ); name = "Supporting Files"; sourceTree = ""; }; - 5B4B125C15227C6E00153613 /* iCloudStoreManagerTests */ = { - isa = PBXGroup; - children = ( - 5B4B126215227C6E00153613 /* iCloudStoreManagerTests.h */, - 5B4B126315227C6E00153613 /* iCloudStoreManagerTests.m */, - 5B4B125D15227C6E00153613 /* Supporting Files */, - ); - path = iCloudStoreManagerTests; - sourceTree = ""; - }; - 5B4B125D15227C6E00153613 /* Supporting Files */ = { + 93D3913AACE1E4369E3900DA /* jrswizzle */ = { isa = PBXGroup; children = ( - 5B4B125E15227C6E00153613 /* iCloudStoreManagerTests-Info.plist */, - 5B4B125F15227C6E00153613 /* InfoPlist.strings */, + 93D39827ACFB3A3A3E1CEF4C /* JRSwizzle.h */, + 93D3982680A3617FB669BBC5 /* JRSwizzle.m */, ); - name = "Supporting Files"; + path = jrswizzle; sourceTree = ""; }; - DA79AA3E1558613F00BAA07A /* iCloudStoreManager */ = { + DA79AA3E1558613F00BAA07A /* UbiquityStoreManager */ = { isa = PBXGroup; children = ( DA79AA3F1558613F00BAA07A /* UbiquityStoreManager.h */, DA79AA401558613F00BAA07A /* UbiquityStoreManager.m */, + 93D39F19900BD420A6E9B72E /* NSError+UbiquityStoreManager.m */, + 93D39B7366D5AD8A2FEFD7F1 /* NSError+UbiquityStoreManager.h */, ); - path = iCloudStoreManager; + path = UbiquityStoreManager; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ - 5B4B122115227C6D00153613 /* iCloudStoreManagerExample */ = { + 5B4B122115227C6D00153613 /* UbiquityStoreManagerExample */ = { isa = PBXNativeTarget; - buildConfigurationList = 5B4B126715227C6E00153613 /* Build configuration list for PBXNativeTarget "iCloudStoreManagerExample" */; + buildConfigurationList = 5B4B126715227C6E00153613 /* Build configuration list for PBXNativeTarget "UbiquityStoreManagerExample" */; buildPhases = ( 5B4B121E15227C6D00153613 /* Sources */, 5B4B121F15227C6D00153613 /* Frameworks */, @@ -214,30 +179,11 @@ ); dependencies = ( ); - name = iCloudStoreManagerExample; - productName = iCloudStoreManagerExample; - productReference = 5B4B122215227C6D00153613 /* iCloudStoreManagerExample.app */; + name = UbiquityStoreManagerExample; + productName = UbiquityStoreManagerExample; + productReference = 5B4B122215227C6D00153613 /* UbiquityStoreManagerExample.app */; productType = "com.apple.product-type.application"; }; - 5B4B125315227C6E00153613 /* iCloudManagerTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 5B4B126A15227C6E00153613 /* Build configuration list for PBXNativeTarget "iCloudManagerTests" */; - buildPhases = ( - 5B4B124F15227C6E00153613 /* Sources */, - 5B4B125015227C6E00153613 /* Frameworks */, - 5B4B125115227C6E00153613 /* Resources */, - 5B4B125215227C6E00153613 /* ShellScript */, - ); - buildRules = ( - ); - dependencies = ( - 5B4B125B15227C6E00153613 /* PBXTargetDependency */, - ); - name = iCloudManagerTests; - productName = iCloudStoreManagerTests; - productReference = 5B4B125415227C6E00153613 /* iCloudManagerTests.octest */; - productType = "com.apple.product-type.bundle"; - }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -245,9 +191,9 @@ isa = PBXProject; attributes = { LastUpgradeCheck = 0430; - ORGANIZATIONNAME = "Yodel Code LLC"; + ORGANIZATIONNAME = Lyndir; }; - buildConfigurationList = 5B4B121C15227C6D00153613 /* Build configuration list for PBXProject "iCloudStoreManagerExample" */; + buildConfigurationList = 5B4B121C15227C6D00153613 /* Build configuration list for PBXProject "UbiquityStoreManagerExample" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = English; hasScannedForEncodings = 0; @@ -259,8 +205,7 @@ projectDirPath = ""; projectRoot = ""; targets = ( - 5B4B122115227C6D00153613 /* iCloudStoreManagerExample */, - 5B4B125315227C6E00153613 /* iCloudManagerTests */, + 5B4B122115227C6D00153613 /* UbiquityStoreManagerExample */, ); }; /* End PBXProject section */ @@ -278,32 +223,8 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 5B4B125115227C6E00153613 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 5B4B126115227C6E00153613 /* InfoPlist.strings in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXResourcesBuildPhase section */ -/* Begin PBXShellScriptBuildPhase section */ - 5B4B125215227C6E00153613 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "# Run the unit tests in this test bundle.\n\"${SYSTEM_DEVELOPER_DIR}/Tools/RunUnitTests\"\n"; - }; -/* End PBXShellScriptBuildPhase section */ - /* Begin PBXSourcesBuildPhase section */ 5B4B121E15227C6D00153613 /* Sources */ = { isa = PBXSourcesBuildPhase; @@ -315,29 +236,15 @@ 5B4B124215227C6E00153613 /* DetailViewController.m in Sources */, 5B4B126F15227DFE00153613 /* User.m in Sources */, 5B4B127215227DFE00153613 /* Event.m in Sources */, - DA79AA3D1558608500BAA07A /* iCloudStoreManagerExample.xcdatamodeld in Sources */, + DA79AA3D1558608500BAA07A /* UbiquityStoreManagerExample.xcdatamodeld in Sources */, DA79AA411558613F00BAA07A /* UbiquityStoreManager.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 5B4B124F15227C6E00153613 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 5B4B126415227C6E00153613 /* iCloudStoreManagerTests.m in Sources */, + 93D39247AA48A93AE9BE359A /* NSError+UbiquityStoreManager.m in Sources */, + 93D3906BB1F143AA15A9BDB9 /* JRSwizzle.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ -/* Begin PBXTargetDependency section */ - 5B4B125B15227C6E00153613 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 5B4B122115227C6D00153613 /* iCloudStoreManagerExample */; - targetProxy = 5B4B125A15227C6E00153613 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - /* Begin PBXVariantGroup section */ 5B4B123115227C6E00153613 /* InfoPlist.strings */ = { isa = PBXVariantGroup; @@ -379,14 +286,6 @@ name = DetailViewController_iPad.xib; sourceTree = ""; }; - 5B4B125F15227C6E00153613 /* InfoPlist.strings */ = { - isa = PBXVariantGroup; - children = ( - 5B4B126015227C6E00153613 /* en */, - ); - name = InfoPlist.strings; - sourceTree = ""; - }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ @@ -410,7 +309,7 @@ GCC_WARN_ABOUT_RETURN_TYPE = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 5.0; + IPHONEOS_DEPLOYMENT_TARGET = 6.0; "PROVISIONING_PROFILE[sdk=iphoneos*]" = ""; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; @@ -430,7 +329,7 @@ GCC_WARN_ABOUT_RETURN_TYPE = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 5.0; + IPHONEOS_DEPLOYMENT_TARGET = 6.0; OTHER_CFLAGS = "-DNS_BLOCK_ASSERTIONS=1"; "PROVISIONING_PROFILE[sdk=iphoneos*]" = ""; SDKROOT = iphoneos; @@ -442,11 +341,12 @@ 5B4B126815227C6E00153613 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - CODE_SIGN_ENTITLEMENTS = iCloudStoreManagerExample/iCloudStoreManagerExample.entitlements; + CODE_SIGN_ENTITLEMENTS = UbiquityStoreManagerExample/UbiquityStoreManagerExample.entitlements; GCC_PRECOMPILE_PREFIX_HEADER = YES; - GCC_PREFIX_HEADER = "iCloudStoreManagerExample/iCloudStoreManagerExample-Prefix.pch"; - INFOPLIST_FILE = "iCloudStoreManagerExample/iCloudStoreManagerExample-Info.plist"; - PRODUCT_NAME = "$(TARGET_NAME)"; + GCC_PREFIX_HEADER = "UbiquityStoreManagerExample/UbiquityStoreManagerExample-Prefix.pch"; + INFOPLIST_FILE = "UbiquityStoreManagerExample/UbiquityStoreManagerExample-Info.plist"; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_NAME = UbiquityStoreManagerExample; WRAPPER_EXTENSION = app; }; name = Debug; @@ -454,53 +354,19 @@ 5B4B126915227C6E00153613 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - CODE_SIGN_ENTITLEMENTS = iCloudStoreManagerExample/iCloudStoreManagerExample.entitlements; + CODE_SIGN_ENTITLEMENTS = UbiquityStoreManagerExample/UbiquityStoreManagerExample.entitlements; GCC_PRECOMPILE_PREFIX_HEADER = YES; - GCC_PREFIX_HEADER = "iCloudStoreManagerExample/iCloudStoreManagerExample-Prefix.pch"; - INFOPLIST_FILE = "iCloudStoreManagerExample/iCloudStoreManagerExample-Info.plist"; - PRODUCT_NAME = "$(TARGET_NAME)"; + GCC_PREFIX_HEADER = "UbiquityStoreManagerExample/UbiquityStoreManagerExample-Prefix.pch"; + INFOPLIST_FILE = "UbiquityStoreManagerExample/UbiquityStoreManagerExample-Info.plist"; + PRODUCT_NAME = UbiquityStoreManagerExample; WRAPPER_EXTENSION = app; }; name = Release; }; - 5B4B126B15227C6E00153613 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/iCloudStoreManagerExample.app/iCloudStoreManagerExample"; - FRAMEWORK_SEARCH_PATHS = ( - "$(SDKROOT)/Developer/Library/Frameworks", - "$(DEVELOPER_LIBRARY_DIR)/Frameworks", - ); - GCC_PRECOMPILE_PREFIX_HEADER = YES; - GCC_PREFIX_HEADER = "iCloudStoreManagerExample/iCloudStoreManagerExample-Prefix.pch"; - INFOPLIST_FILE = "iCloudStoreManagerTests/iCloudStoreManagerTests-Info.plist"; - PRODUCT_NAME = "$(TARGET_NAME)"; - TEST_HOST = "$(BUNDLE_LOADER)"; - WRAPPER_EXTENSION = octest; - }; - name = Debug; - }; - 5B4B126C15227C6E00153613 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/iCloudStoreManagerExample.app/iCloudStoreManagerExample"; - FRAMEWORK_SEARCH_PATHS = ( - "$(SDKROOT)/Developer/Library/Frameworks", - "$(DEVELOPER_LIBRARY_DIR)/Frameworks", - ); - GCC_PRECOMPILE_PREFIX_HEADER = YES; - GCC_PREFIX_HEADER = "iCloudStoreManagerExample/iCloudStoreManagerExample-Prefix.pch"; - INFOPLIST_FILE = "iCloudStoreManagerTests/iCloudStoreManagerTests-Info.plist"; - PRODUCT_NAME = "$(TARGET_NAME)"; - TEST_HOST = "$(BUNDLE_LOADER)"; - WRAPPER_EXTENSION = octest; - }; - name = Release; - }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - 5B4B121C15227C6D00153613 /* Build configuration list for PBXProject "iCloudStoreManagerExample" */ = { + 5B4B121C15227C6D00153613 /* Build configuration list for PBXProject "UbiquityStoreManagerExample" */ = { isa = XCConfigurationList; buildConfigurations = ( 5B4B126515227C6E00153613 /* Debug */, @@ -509,7 +375,7 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 5B4B126715227C6E00153613 /* Build configuration list for PBXNativeTarget "iCloudStoreManagerExample" */ = { + 5B4B126715227C6E00153613 /* Build configuration list for PBXNativeTarget "UbiquityStoreManagerExample" */ = { isa = XCConfigurationList; buildConfigurations = ( 5B4B126815227C6E00153613 /* Debug */, @@ -518,25 +384,16 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 5B4B126A15227C6E00153613 /* Build configuration list for PBXNativeTarget "iCloudManagerTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 5B4B126B15227C6E00153613 /* Debug */, - 5B4B126C15227C6E00153613 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; /* End XCConfigurationList section */ /* Begin XCVersionGroup section */ - DA79AA3B1558608500BAA07A /* iCloudStoreManagerExample.xcdatamodeld */ = { + DA79AA3B1558608500BAA07A /* UbiquityStoreManagerExample.xcdatamodeld */ = { isa = XCVersionGroup; children = ( - DA79AA3C1558608500BAA07A /* iCloudStoreManager.xcdatamodel */, + DA79AA3C1558608500BAA07A /* UbiquityStoreManager.xcdatamodel */, ); - currentVersion = DA79AA3C1558608500BAA07A /* iCloudStoreManager.xcdatamodel */; - path = iCloudStoreManagerExample.xcdatamodeld; + currentVersion = DA79AA3C1558608500BAA07A /* UbiquityStoreManager.xcdatamodel */; + path = UbiquityStoreManagerExample.xcdatamodeld; sourceTree = ""; versionGroupType = wrapper.xcdatamodel; }; diff --git a/iCloudStoreManagerExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/UbiquityStoreManagerExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 63% rename from iCloudStoreManagerExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to UbiquityStoreManagerExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 5e80647..973a379 100644 --- a/iCloudStoreManagerExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/UbiquityStoreManagerExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -2,6 +2,6 @@ + location = "self:UbiquityStoreManagerExample.xcodeproj"> diff --git a/iCloudStoreManagerExample.xcodeproj/project.xcworkspace/xcuserdata/aleksey.xcuserdatad/UserInterfaceState.xcuserstate b/UbiquityStoreManagerExample.xcodeproj/project.xcworkspace/xcuserdata/aleksey.xcuserdatad/UserInterfaceState.xcuserstate similarity index 100% rename from iCloudStoreManagerExample.xcodeproj/project.xcworkspace/xcuserdata/aleksey.xcuserdatad/UserInterfaceState.xcuserstate rename to UbiquityStoreManagerExample.xcodeproj/project.xcworkspace/xcuserdata/aleksey.xcuserdatad/UserInterfaceState.xcuserstate diff --git a/iCloudStoreManagerExample.xcodeproj/project.xcworkspace/xcuserdata/aleksey.xcuserdatad/WorkspaceSettings.xcsettings b/UbiquityStoreManagerExample.xcodeproj/project.xcworkspace/xcuserdata/aleksey.xcuserdatad/WorkspaceSettings.xcsettings similarity index 100% rename from iCloudStoreManagerExample.xcodeproj/project.xcworkspace/xcuserdata/aleksey.xcuserdatad/WorkspaceSettings.xcsettings rename to UbiquityStoreManagerExample.xcodeproj/project.xcworkspace/xcuserdata/aleksey.xcuserdatad/WorkspaceSettings.xcsettings diff --git a/UbiquityStoreManagerExample.xcodeproj/xcshareddata/xcschemes/UbiquityStoreManagerExample.xcscheme b/UbiquityStoreManagerExample.xcodeproj/xcshareddata/xcschemes/UbiquityStoreManagerExample.xcscheme new file mode 100644 index 0000000..d02a089 --- /dev/null +++ b/UbiquityStoreManagerExample.xcodeproj/xcshareddata/xcschemes/UbiquityStoreManagerExample.xcscheme @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iCloudStoreManagerExample/AppDelegate.h b/UbiquityStoreManagerExample/AppDelegate.h similarity index 77% rename from iCloudStoreManagerExample/AppDelegate.h rename to UbiquityStoreManagerExample/AppDelegate.h index c0bdc83..ce36ac7 100644 --- a/iCloudStoreManagerExample/AppDelegate.h +++ b/UbiquityStoreManagerExample/AppDelegate.h @@ -1,6 +1,6 @@ // // AppDelegate.h -// iCloudStoreManagerExample +// UbiquityStoreManagerExample // // Created by Aleksey Novicov on 3/27/12. // Copyright (c) 2012 Yodel Code LLC. All rights reserved. @@ -11,13 +11,12 @@ @class User; -@interface AppDelegate : UIResponder +@interface AppDelegate : UIResponder @property (strong, nonatomic) UIWindow *window; @property (readonly, strong, nonatomic) NSManagedObjectContext *managedObjectContext; @property (readonly, strong, nonatomic) NSManagedObjectModel *managedObjectModel; -@property (readonly, strong, nonatomic) NSPersistentStoreCoordinator *persistentStoreCoordinator; - (void)saveContext; - (NSURL *)applicationDocumentsDirectory; diff --git a/iCloudStoreManagerExample/AppDelegate.m b/UbiquityStoreManagerExample/AppDelegate.m similarity index 50% rename from iCloudStoreManagerExample/AppDelegate.m rename to UbiquityStoreManagerExample/AppDelegate.m index 2cefd8d..8dcd269 100644 --- a/iCloudStoreManagerExample/AppDelegate.m +++ b/UbiquityStoreManagerExample/AppDelegate.m @@ -1,11 +1,12 @@ // // AppDelegate.m -// iCloudStoreManagerExample +// UbiquityStoreManagerExample // // Created by Aleksey Novicov on 3/27/12. // Copyright (c) 2012 Yodel Code LLC. All rights reserved. // +#import #import "AppDelegate.h" #import "MasterViewController.h" #import "DetailViewController.h" @@ -13,6 +14,11 @@ #import "User.h" @interface AppDelegate () +@property(nonatomic, strong) UIAlertView *handleCloudContentAlert; + +@property(nonatomic, strong) UIAlertView *handleCloudContentWarningAlert; + +@property(nonatomic, strong) UIAlertView *handleLocalStoreAlert; - (NSURL *)storeURL; @end @@ -24,7 +30,6 @@ @implementation AppDelegate { @synthesize window = _window; @synthesize managedObjectContext = __managedObjectContext; @synthesize managedObjectModel = __managedObjectModel; -@synthesize persistentStoreCoordinator = __persistentStoreCoordinator; @synthesize navigationController = _navigationController; @synthesize splitViewController = _splitViewController; @synthesize ubiquityStoreManager; @@ -35,24 +40,19 @@ + (AppDelegate *)appDelegate { - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - // STEP 1 - Initialize the UbiquityStoreManager - ubiquityStoreManager = [[UbiquityStoreManager alloc] initWithManagedObjectModel: [self managedObjectModel] - localStoreURL: [self storeURL] - containerIdentifier: nil - additionalStoreOptions: nil]; - // STEP 2a - Setup the delegate - ubiquityStoreManager.delegate = self; - - // For test purposes only. NOT FOR USE IN PRODUCTION - ubiquityStoreManager.hardResetEnabled = YES; + NSLog(@"Starting UbiquityStoreManagerExample on device: %@\n\n", [UIDevice currentDevice].name); + // STEP 1 - Initialize the UbiquityStoreManager + ubiquityStoreManager = [[UbiquityStoreManager alloc] initStoreNamed:nil withManagedObjectModel:[self managedObjectModel] + localStoreURL:[self storeURL] containerIdentifier:nil additionalStoreOptions:nil + delegate:self]; + self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; // Override point for customization after application launch. if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) { masterViewController = [[MasterViewController alloc] initWithNibName:@"MasterViewController_iPhone" bundle:nil]; self.navigationController = [[UINavigationController alloc] initWithRootViewController:masterViewController]; self.window.rootViewController = self.navigationController; - masterViewController.managedObjectContext = self.managedObjectContext; } else { masterViewController = [[MasterViewController alloc] initWithNibName:@"MasterViewController_iPad" bundle:nil]; UINavigationController *masterNavigationController = [[UINavigationController alloc] initWithRootViewController:masterViewController]; @@ -67,7 +67,6 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( self.splitViewController.viewControllers = [NSArray arrayWithObjects:masterNavigationController, detailNavigationController, nil]; self.window.rootViewController = self.splitViewController; - masterViewController.managedObjectContext = self.managedObjectContext; } [self.window makeKeyAndVisible]; return YES; @@ -86,8 +85,6 @@ - (void)applicationDidEnterBackground:(UIApplication *)application - (void)applicationWillEnterForeground:(UIApplication *)application { - // STEP 2b - Check to make sure user has not deleted the iCloud data from Settings - [self.ubiquityStoreManager checkiCloudStatus]; } - (void)applicationDidBecomeActive:(UIApplication *)application @@ -122,36 +119,13 @@ - (void)saveContext { #pragma mark - Core Data stack -// Returns the managed object context for the application. -// If the context doesn't already exist, it is created and bound to the persistent store coordinator for the application. -- (NSManagedObjectContext *)managedObjectContext { - - if (__managedObjectContext != nil) { - return __managedObjectContext; - } - - NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator]; - - if (coordinator != nil) { - NSManagedObjectContext* moc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType]; - - [moc performBlockAndWait:^{ - [moc setPersistentStoreCoordinator: coordinator]; - }]; - - __managedObjectContext = moc; - __managedObjectContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy; - } - return __managedObjectContext; -} - // Returns the managed object model for the application. // If the model doesn't already exist, it is created from the application's model. - (NSManagedObjectModel *)managedObjectModel { if (__managedObjectModel != nil) { return __managedObjectModel; } - NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"iCloudStoreManagerExample" withExtension:@"momd"]; + NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"UbiquityStoreManagerExample" withExtension:@"momd"]; __managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL]; return __managedObjectModel; } @@ -160,18 +134,6 @@ - (NSURL *)storeURL { return [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"Sample.sqlite"]; } -// Returns the persistent store coordinator for the application. -// If the coordinator doesn't already exist, it is created and the application's store added to it. -- (NSPersistentStoreCoordinator *)persistentStoreCoordinator { - if (__persistentStoreCoordinator == nil) { - - // STEP 3 - Get the persistentStoreCoordinator from the UbiquityStoreManager - __persistentStoreCoordinator = [ubiquityStoreManager persistentStoreCoordinator]; - } - - return __persistentStoreCoordinator; -} - #pragma mark - Application's Documents directory // Returns the URL to the application's Documents directory. @@ -194,16 +156,104 @@ - (User *)primaryUser { return primaryUser; } +#pragma mark - UIAlertViewDelegate + +- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex { + + if (alertView == self.handleCloudContentAlert && buttonIndex == [alertView firstOtherButtonIndex]) { + // Fix Now + self.handleCloudContentWarningAlert = [[UIAlertView alloc] initWithTitle:@"Fix iCloud Now" message: + @"This problem can usually be auto‑corrected by opening the app on another device where you recently made changes.\n" + @"If you wish to correct the problem from this device anyway, it is possible that recent changes on another device will be lost." + delegate:self + cancelButtonTitle:@"Back" + otherButtonTitles:@"Fix Anyway", nil]; + [self.handleCloudContentWarningAlert show]; + } + + if (alertView == self.handleCloudContentWarningAlert) { + if (buttonIndex == alertView.cancelButtonIndex) + // Back + [self.handleCloudContentAlert show]; + + if (buttonIndex == alertView.firstOtherButtonIndex) + // Fix Anyway + [self.ubiquityStoreManager rebuildCloudContentFromCloudStoreOrLocalStore:YES]; + } + + if (alertView == self.handleLocalStoreAlert && buttonIndex == [alertView firstOtherButtonIndex]) + [self.ubiquityStoreManager deleteLocalStore]; +} + + #pragma mark - UbiquityStoreManagerDelegate // STEP 4 - Implement the UbiquityStoreManager delegate methods +- (NSManagedObjectContext *)managedObjectContextForUbiquityChangesInManager:(UbiquityStoreManager *)manager { + return self.managedObjectContext; +} + +- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager willLoadStoreIsCloud:(BOOL)isCloudStore { -- (NSManagedObjectContext *)managedObjectContextForUbiquityStoreManager:(UbiquityStoreManager *)usm { - return self.managedObjectContext; + __managedObjectContext = nil; + dispatch_async(dispatch_get_main_queue(), ^{ + [masterViewController.iCloudSwitch setOn:isCloudStore animated:YES]; + [masterViewController.storeLoadingActivity startAnimating]; + }); } -- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didSwitchToiCloud:(BOOL)didSwitch { - [masterViewController.iCloudSwitch setOn:didSwitch animated:YES]; +- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager failedLoadingStoreWithCause:(UbiquityStoreErrorCause)cause context:(id)context wasCloud:(BOOL)wasCloudStore { + + dispatch_async(dispatch_get_main_queue(), ^{ + [masterViewController.storeLoadingActivity stopAnimating]; + + if (!wasCloudStore && ![self.handleLocalStoreAlert isVisible]) { + self.handleLocalStoreAlert = [[UIAlertView alloc] initWithTitle:@"Local Store Problem" message: + @"Your datastore got corrupted and needs to be recreated." + delegate:self + cancelButtonTitle:nil otherButtonTitles:@"Recreate", nil]; + [self.handleLocalStoreAlert show]; + } + }); +} + +- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didLoadStoreForCoordinator:(NSPersistentStoreCoordinator *)coordinator isCloud:(BOOL)isCloudStore { + + NSManagedObjectContext *moc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType]; + [moc setPersistentStoreCoordinator:coordinator]; + + __managedObjectContext = moc; + __managedObjectContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy; + dispatch_async(dispatch_get_main_queue(), ^{ + [self.handleCloudContentAlert dismissWithClickedButtonIndex:[self.handleCloudContentAlert firstOtherButtonIndex] + 9 + animated:YES]; + [self.handleCloudContentWarningAlert dismissWithClickedButtonIndex:[self.handleCloudContentWarningAlert firstOtherButtonIndex] + 9 + animated:YES]; + + [masterViewController.iCloudSwitch setOn:isCloudStore animated:YES]; + [masterViewController.storeLoadingActivity stopAnimating]; + }); +} + +- (BOOL)ubiquityStoreManager:(UbiquityStoreManager *)manager handleCloudContentCorruptionWithHealthyStore:(BOOL)storeHealthy { + + if ([self.handleCloudContentAlert isVisible] || [self.handleCloudContentWarningAlert isVisible]) + NSLog(@"already showing."); + else if (manager.cloudEnabled && !storeHealthy) + dispatch_async(dispatch_get_main_queue(), ^{ + self.handleCloudContentAlert = [[UIAlertView alloc] initWithTitle:@"iCloud Sync Problem" message: + @"\n\n\n\nWaiting for another device to auto‑correct the problem..." + delegate:self + cancelButtonTitle:nil otherButtonTitles:@"Fix Now", nil]; + UIActivityIndicatorView *activityIndicator = [[UIActivityIndicatorView alloc] + initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge]; + activityIndicator.center = CGPointMake( 142, 90 ); + [activityIndicator startAnimating]; + [self.handleCloudContentAlert addSubview:activityIndicator]; + [self.handleCloudContentAlert show]; + }); + + return NO; } @end diff --git a/iCloudStoreManagerExample/DetailViewController.h b/UbiquityStoreManagerExample/DetailViewController.h similarity index 93% rename from iCloudStoreManagerExample/DetailViewController.h rename to UbiquityStoreManagerExample/DetailViewController.h index 2a75c7a..160133f 100644 --- a/iCloudStoreManagerExample/DetailViewController.h +++ b/UbiquityStoreManagerExample/DetailViewController.h @@ -1,6 +1,6 @@ // // DetailViewController.h -// iCloudStoreManagerExample +// UbiquityStoreManagerExample // // Created by Aleksey Novicov on 3/27/12. // Copyright (c) 2012 Yodel Code LLC. All rights reserved. diff --git a/iCloudStoreManagerExample/DetailViewController.m b/UbiquityStoreManagerExample/DetailViewController.m similarity index 96% rename from iCloudStoreManagerExample/DetailViewController.m rename to UbiquityStoreManagerExample/DetailViewController.m index 8c095a4..c306920 100644 --- a/iCloudStoreManagerExample/DetailViewController.m +++ b/UbiquityStoreManagerExample/DetailViewController.m @@ -1,6 +1,6 @@ // // DetailViewController.m -// iCloudStoreManagerExample +// UbiquityStoreManagerExample // // Created by Aleksey Novicov on 3/27/12. // Copyright (c) 2012 Yodel Code LLC. All rights reserved. @@ -86,10 +86,7 @@ - (void)splitViewController:(UISplitViewController *)splitController willShowVie #pragma mark - Table View - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { - if ([[AppDelegate appDelegate] ubiquityStoreManager].isReady) - return 1; - else - return 0; + return 1; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { diff --git a/iCloudStoreManagerExample/Event.h b/UbiquityStoreManagerExample/Event.h similarity index 91% rename from iCloudStoreManagerExample/Event.h rename to UbiquityStoreManagerExample/Event.h index 5c2bf83..17d7a85 100644 --- a/iCloudStoreManagerExample/Event.h +++ b/UbiquityStoreManagerExample/Event.h @@ -1,6 +1,6 @@ // // Event.h -// iCloudStoreManagerExample +// UbiquityStoreManagerExample // // Created by Aleksey Novicov on 3/27/12. // Copyright (c) 2012 Yodel Code LLC. All rights reserved. diff --git a/iCloudStoreManagerExample/Event.m b/UbiquityStoreManagerExample/Event.m similarity index 87% rename from iCloudStoreManagerExample/Event.m rename to UbiquityStoreManagerExample/Event.m index 9962e6c..54e0091 100644 --- a/iCloudStoreManagerExample/Event.m +++ b/UbiquityStoreManagerExample/Event.m @@ -1,6 +1,6 @@ // // Event.m -// iCloudStoreManagerExample +// UbiquityStoreManagerExample // // Created by Aleksey Novicov on 3/27/12. // Copyright (c) 2012 Yodel Code LLC. All rights reserved. diff --git a/iCloudStoreManagerExample/MasterViewController.h b/UbiquityStoreManagerExample/MasterViewController.h similarity index 82% rename from iCloudStoreManagerExample/MasterViewController.h rename to UbiquityStoreManagerExample/MasterViewController.h index 3b798e7..1fe0122 100644 --- a/iCloudStoreManagerExample/MasterViewController.h +++ b/UbiquityStoreManagerExample/MasterViewController.h @@ -1,6 +1,6 @@ // // MasterViewController.h -// iCloudStoreManagerExample +// UbiquityStoreManagerExample // // Created by Aleksey Novicov on 3/27/12. // Copyright (c) 2012 Yodel Code LLC. All rights reserved. @@ -17,12 +17,13 @@ @property (strong, nonatomic) DetailViewController *detailViewController; @property (strong, nonatomic) NSFetchedResultsController *fetchedResultsController; -@property (strong, nonatomic) NSManagedObjectContext *managedObjectContext; @property (strong, nonatomic) IBOutlet UISwitch *iCloudSwitch; @property (strong, nonatomic) IBOutlet UIView *tableHeaderView; @property (strong, nonatomic) IBOutlet UIButton *clearButton; +@property (strong, nonatomic) IBOutlet UIActivityIndicatorView *storeLoadingActivity; - (IBAction)setiCloudState:(id)sender; - (IBAction)cleariCloud:(id)sender; +- (IBAction)rebuildiCloud:(id)sender; @end diff --git a/iCloudStoreManagerExample/MasterViewController.m b/UbiquityStoreManagerExample/MasterViewController.m similarity index 72% rename from iCloudStoreManagerExample/MasterViewController.m rename to UbiquityStoreManagerExample/MasterViewController.m index d9421e5..c038fa9 100644 --- a/iCloudStoreManagerExample/MasterViewController.m +++ b/UbiquityStoreManagerExample/MasterViewController.m @@ -1,6 +1,6 @@ // // MasterViewController.m -// iCloudStoreManagerExample +// UbiquityStoreManagerExample // // Created by Aleksey Novicov on 3/27/12. // Copyright (c) 2012 Yodel Code LLC. All rights reserved. @@ -20,7 +20,6 @@ @implementation MasterViewController @synthesize detailViewController = _detailViewController; @synthesize fetchedResultsController = __fetchedResultsController; -@synthesize managedObjectContext = __managedObjectContext; @synthesize iCloudSwitch; @synthesize clearButton; @synthesize tableHeaderView; @@ -29,14 +28,18 @@ - (IBAction)setiCloudState:(id)sender { UISwitch *aSwitch = sender; // STEP 5a - Set the state of the UbiquityStoreManager to reflect the current UI - [[[AppDelegate appDelegate] ubiquityStoreManager] useiCloudStore:aSwitch.on alertUser:YES]; + [[[AppDelegate appDelegate] ubiquityStoreManager] setCloudEnabled:aSwitch.on]; } - (IBAction)cleariCloud:(id)sender { - iCloudSwitch.on = NO; // STEP 6 - UbiquityStoreManager hard reset. FOR TESTING ONLY! Do not expose to the end user! - [[[AppDelegate appDelegate] ubiquityStoreManager] hardResetCloudStorage]; + [[[AppDelegate appDelegate] ubiquityStoreManager] deleteCloudContainerLocalOnly:NO]; +} + +- (IBAction)rebuildiCloud:(id)sender { + + [[[AppDelegate appDelegate] ubiquityStoreManager] deleteCloudContainerLocalOnly:YES]; } - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil @@ -54,22 +57,17 @@ - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil - (void)reloadFetchedResults:(NSNotification*)note { - // STEP 7a - Do not allow use of any NSManagedObjectContext until UbiquityStoreManager is ready - - if ([[AppDelegate appDelegate] ubiquityStoreManager].isReady) { - - // Refetch the data - self.fetchedResultsController = nil; - [self fetchedResultsController]; - - if (note) { - [self.tableView reloadData]; - - // STEP 5b - Display current state of the UbiquityStoreManager - BOOL enabled = [[AppDelegate appDelegate] ubiquityStoreManager].iCloudEnabled; - [iCloudSwitch setOn:enabled animated:YES]; - } - } + // Refetch the data + self.fetchedResultsController = nil; + [self fetchedResultsController]; + + if (note) { + [self.tableView reloadData]; + + // STEP 5b - Display current state of the UbiquityStoreManager + BOOL enabled = [[AppDelegate appDelegate] ubiquityStoreManager].cloudEnabled; + [iCloudSwitch setOn:enabled animated:YES]; + } } - (void)viewDidLoad { @@ -81,25 +79,17 @@ - (void)viewDidLoad { UIBarButtonItem *addButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(insertNewObject:)]; self.navigationItem.rightBarButtonItem = addButton; - // iCloud support - [self reloadFetchedResults:nil]; - // Observe the app delegate telling us when it's finished asynchronously setting up the persistent store [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(reloadFetchedResults:) - name: RefetchAllDatabaseDataNotificationKey + name: UbiquityManagedStoreDidChangeNotification object: [[AppDelegate appDelegate] ubiquityStoreManager]]; - + self.tableView.tableHeaderView = self.tableHeaderView; // STEP 5c - Display current state of the UbiquityStoreManager - self.iCloudSwitch.on = [[AppDelegate appDelegate] ubiquityStoreManager].iCloudEnabled; -} - -- (void)viewDidUnload -{ - [super viewDidUnload]; - // Release any retained subviews of the main view. + self.iCloudSwitch.on = [[AppDelegate appDelegate] ubiquityStoreManager].cloudEnabled; + [self reloadFetchedResults:nil]; } - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation @@ -141,11 +131,7 @@ - (void)insertNewObject:(id)sender - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { // STEP 7b - Do not allow use of any NSManagedObjectContext until UbiquityStoreManager is ready - - if ([[AppDelegate appDelegate] ubiquityStoreManager].isReady) - return [[self.fetchedResultsController sections] count]; - else - return 0; + return [[self.fetchedResultsController sections] count]; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section @@ -216,18 +202,17 @@ - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath } else { self.detailViewController.detailItem = object; } - self.detailViewController.fileList = [[[AppDelegate appDelegate] ubiquityStoreManager] fileList]; + self.detailViewController.fileList = [[NSFileManager defaultManager] subpathsAtPath:[[[AppDelegate appDelegate] ubiquityStoreManager].URLForCloudContainer path]]; [self.detailViewController.tableView reloadData]; } #pragma mark - Fetched results controller - (NSFetchedResultsController *)fetchedResultsController { - - // STEP 7c - Do not allow use of any NSManagedObjectContext until UbiquityStoreManager is ready - - if (![[AppDelegate appDelegate] ubiquityStoreManager].isReady) - return nil; + + if ([AppDelegate appDelegate].managedObjectContext == nil) { + return nil; + } if (__fetchedResultsController != nil) { return __fetchedResultsController; @@ -235,7 +220,7 @@ - (NSFetchedResultsController *)fetchedResultsController { NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; // Edit the entity name as appropriate. - NSEntityDescription *entity = [NSEntityDescription entityForName:@"Event" inManagedObjectContext:self.managedObjectContext]; + NSEntityDescription *entity = [NSEntityDescription entityForName:@"Event" inManagedObjectContext:[AppDelegate appDelegate].managedObjectContext]; [fetchRequest setEntity:entity]; // Set the batch size to a suitable number. @@ -249,11 +234,11 @@ - (NSFetchedResultsController *)fetchedResultsController { // Edit the section name key path and cache name if appropriate. // nil for section name key path means "no sections". - NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil cacheName:@"Master"]; + NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:[AppDelegate appDelegate].managedObjectContext sectionNameKeyPath:nil cacheName:@"Master"]; aFetchedResultsController.delegate = self; self.fetchedResultsController = aFetchedResultsController; - [self.managedObjectContext performBlockAndWait:^{ + [[AppDelegate appDelegate].managedObjectContext performBlockAndWait:^{ NSError *error = nil; if (![self.fetchedResultsController performFetch:&error]) { // Replace this implementation with code to handle the error appropriately. @@ -266,54 +251,54 @@ - (NSFetchedResultsController *)fetchedResultsController { return __fetchedResultsController; } -- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller -{ - [self.tableView beginUpdates]; -} - -- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id )sectionInfo - atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type -{ - switch(type) { - case NSFetchedResultsChangeInsert: - [self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade]; - break; - - case NSFetchedResultsChangeDelete: - [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade]; - break; - } -} - -- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject - atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type - newIndexPath:(NSIndexPath *)newIndexPath -{ - UITableView *tableView = self.tableView; - - switch(type) { - case NSFetchedResultsChangeInsert: - [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade]; - break; - - case NSFetchedResultsChangeDelete: - [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade]; - break; - - case NSFetchedResultsChangeUpdate: - [self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath]; - break; - - case NSFetchedResultsChangeMove: - [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade]; - [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]withRowAnimation:UITableViewRowAnimationFade]; - break; - } -} +//- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller +//{ +// [self.tableView beginUpdates]; +//} +// +//- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id )sectionInfo +// atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type +//{ +// switch(type) { +// case NSFetchedResultsChangeInsert: +// [self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade]; +// break; +// +// case NSFetchedResultsChangeDelete: +// [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade]; +// break; +// } +//} +// +//- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject +// atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type +// newIndexPath:(NSIndexPath *)newIndexPath +//{ +// UITableView *tableView = self.tableView; +// +// switch(type) { +// case NSFetchedResultsChangeInsert: +// [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade]; +// break; +// +// case NSFetchedResultsChangeDelete: +// [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade]; +// break; +// +// case NSFetchedResultsChangeUpdate: +// [self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath]; +// break; +// +// case NSFetchedResultsChangeMove: +// [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade]; +// [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]withRowAnimation:UITableViewRowAnimationFade]; +// break; +// } +//} - (void)controllerDidChangeContent:(NSFetchedResultsController *)controller { - [self.tableView endUpdates]; + [self.tableView reloadData]; } /* diff --git a/iCloudStoreManagerExample/iCloudStoreManagerExample-Info.plist b/UbiquityStoreManagerExample/UbiquityStoreManagerExample-Info.plist similarity index 95% rename from iCloudStoreManagerExample/iCloudStoreManagerExample-Info.plist rename to UbiquityStoreManagerExample/UbiquityStoreManagerExample-Info.plist index 2900d33..d3f0d0a 100644 --- a/iCloudStoreManagerExample/iCloudStoreManagerExample-Info.plist +++ b/UbiquityStoreManagerExample/UbiquityStoreManagerExample-Info.plist @@ -9,7 +9,7 @@ CFBundleExecutable ${EXECUTABLE_NAME} CFBundleIdentifier - com.yodelcode.iCloudManager + com.lyndir.lhunath.UbiquityStoreManagerExample CFBundleInfoDictionaryVersion 6.0 CFBundleName diff --git a/iCloudStoreManagerExample/iCloudStoreManagerExample-Prefix.pch b/UbiquityStoreManagerExample/UbiquityStoreManagerExample-Prefix.pch similarity index 66% rename from iCloudStoreManagerExample/iCloudStoreManagerExample-Prefix.pch rename to UbiquityStoreManagerExample/UbiquityStoreManagerExample-Prefix.pch index 139bf51..774efa7 100644 --- a/iCloudStoreManagerExample/iCloudStoreManagerExample-Prefix.pch +++ b/UbiquityStoreManagerExample/UbiquityStoreManagerExample-Prefix.pch @@ -1,5 +1,5 @@ // -// Prefix header for all source files of the 'iCloudStoreManagerExample' target in the 'iCloudStoreManagerExample' project +// Prefix header for all source files of the 'UbiquityStoreManagerExample' target in the 'UbiquityStoreManagerExample' project // #import diff --git a/iCloudStoreManagerExample/iCloudManager.entitlements b/UbiquityStoreManagerExample/UbiquityStoreManagerExample.entitlements similarity index 59% rename from iCloudStoreManagerExample/iCloudManager.entitlements rename to UbiquityStoreManagerExample/UbiquityStoreManagerExample.entitlements index b754656..27f66f9 100644 --- a/iCloudStoreManagerExample/iCloudManager.entitlements +++ b/UbiquityStoreManagerExample/UbiquityStoreManagerExample.entitlements @@ -4,13 +4,13 @@ com.apple.developer.ubiquity-container-identifiers - $(TeamIdentifierPrefix)com.yodelcode.icloudstoremanager + $(TeamIdentifierPrefix)com.lyndir.lhunath.UbiquityStoreManagerExample com.apple.developer.ubiquity-kvstore-identifier - $(TeamIdentifierPrefix)com.yodelcode.icloudstoremanager + $(TeamIdentifierPrefix)com.lyndir.lhunath.UbiquityStoreManagerExample keychain-access-groups - $(AppIdentifierPrefix)com.yodelcode.icloudstoremanager.iCloudStoreManagerExample + $(AppIdentifierPrefix)com.lyndir.lhunath.UbiquityStoreManagerExample diff --git a/iCloudStoreManagerExample/iCloudStoreManagerExample.xcdatamodeld/.xccurrentversion b/UbiquityStoreManagerExample/UbiquityStoreManagerExample.xcdatamodeld/.xccurrentversion similarity index 81% rename from iCloudStoreManagerExample/iCloudStoreManagerExample.xcdatamodeld/.xccurrentversion rename to UbiquityStoreManagerExample/UbiquityStoreManagerExample.xcdatamodeld/.xccurrentversion index a271510..a6cfe4a 100644 --- a/iCloudStoreManagerExample/iCloudStoreManagerExample.xcdatamodeld/.xccurrentversion +++ b/UbiquityStoreManagerExample/UbiquityStoreManagerExample.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - iCloudStoreManager.xcdatamodel + UbiquityStoreManager.xcdatamodel diff --git a/iCloudStoreManagerExample/iCloudStoreManagerExample.xcdatamodeld/iCloudStoreManager.xcdatamodel/contents b/UbiquityStoreManagerExample/UbiquityStoreManagerExample.xcdatamodeld/iCloudStoreManager.xcdatamodel/contents similarity index 100% rename from iCloudStoreManagerExample/iCloudStoreManagerExample.xcdatamodeld/iCloudStoreManager.xcdatamodel/contents rename to UbiquityStoreManagerExample/UbiquityStoreManagerExample.xcdatamodeld/iCloudStoreManager.xcdatamodel/contents diff --git a/iCloudStoreManagerExample/User.h b/UbiquityStoreManagerExample/User.h similarity index 96% rename from iCloudStoreManagerExample/User.h rename to UbiquityStoreManagerExample/User.h index a9e629b..9b66b55 100644 --- a/iCloudStoreManagerExample/User.h +++ b/UbiquityStoreManagerExample/User.h @@ -1,6 +1,6 @@ // // User.h -// iCloudStoreManagerExample +// UbiquityStoreManagerExample // // Created by Aleksey Novicov on 3/27/12. // Copyright (c) 2012 Yodel Code LLC. All rights reserved. diff --git a/iCloudStoreManagerExample/User.m b/UbiquityStoreManagerExample/User.m similarity index 97% rename from iCloudStoreManagerExample/User.m rename to UbiquityStoreManagerExample/User.m index c6f72b7..e3d3e6e 100644 --- a/iCloudStoreManagerExample/User.m +++ b/UbiquityStoreManagerExample/User.m @@ -1,6 +1,6 @@ // // User.m -// iCloudStoreManagerExample +// UbiquityStoreManagerExample // // Created by Aleksey Novicov on 3/27/12. // Copyright (c) 2012 Yodel Code LLC. All rights reserved. diff --git a/iCloudStoreManagerExample/en.lproj/DetailViewController_iPad.xib b/UbiquityStoreManagerExample/en.lproj/DetailViewController_iPad.xib similarity index 100% rename from iCloudStoreManagerExample/en.lproj/DetailViewController_iPad.xib rename to UbiquityStoreManagerExample/en.lproj/DetailViewController_iPad.xib diff --git a/iCloudStoreManagerExample/en.lproj/DetailViewController_iPhone.xib b/UbiquityStoreManagerExample/en.lproj/DetailViewController_iPhone.xib similarity index 100% rename from iCloudStoreManagerExample/en.lproj/DetailViewController_iPhone.xib rename to UbiquityStoreManagerExample/en.lproj/DetailViewController_iPhone.xib diff --git a/iCloudStoreManagerExample/en.lproj/InfoPlist.strings b/UbiquityStoreManagerExample/en.lproj/InfoPlist.strings similarity index 100% rename from iCloudStoreManagerExample/en.lproj/InfoPlist.strings rename to UbiquityStoreManagerExample/en.lproj/InfoPlist.strings diff --git a/iCloudStoreManagerExample/en.lproj/MasterViewController_iPad.xib b/UbiquityStoreManagerExample/en.lproj/MasterViewController_iPad.xib similarity index 75% rename from iCloudStoreManagerExample/en.lproj/MasterViewController_iPad.xib rename to UbiquityStoreManagerExample/en.lproj/MasterViewController_iPad.xib index f0893b5..2050f3f 100644 --- a/iCloudStoreManagerExample/en.lproj/MasterViewController_iPad.xib +++ b/UbiquityStoreManagerExample/en.lproj/MasterViewController_iPad.xib @@ -1,22 +1,23 @@ - 1296 - 11D50 - 2182 - 1138.32 - 568.00 + 1552 + 12C60 + 3084 + 1187.34 + 625.00 com.apple.InterfaceBuilder.IBCocoaTouchPlugin - 1179 + 2083 + IBProxyObject + IBUIActivityIndicatorView IBUIButton - IBUITableView + IBUILabel IBUISwitch + IBUITableView IBUIView - IBUILabel - IBProxyObject com.apple.InterfaceBuilder.IBCocoaTouchPlugin @@ -39,7 +40,6 @@ 274 {{0, 20}, {320, 832}} - 3 MQA @@ -85,10 +85,9 @@ 292 - {{146, 20}, {94, 27}} + {{156, 20}, {94, 27}} - - + _NS:9 NO IBIPadFramework @@ -99,9 +98,8 @@ 292 - {{81, 22}, {129, 24}} + {{30, 22}, {123, 24}} - _NS:9 NO @@ -113,10 +111,12 @@ 1 MCAwIDAAA + darkTextColor 0 10 + 2 2 20 @@ -127,42 +127,72 @@ 16 - + + + -2147483356 + {{256, 24}, {20, 20}} + + _NS:9 + NO + IBIPadFramework + 2 + + 292 - {{81, 73}, {159, 37}} + {{171, 67}, {130, 44}} - _NS:9 NO IBIPadFramework 0 0 1 - Clear iCloud Data + Rebuild iCloud 1 MC4xOTYwNzg0MzQ2IDAuMzA5ODAzOTMyOSAwLjUyMTU2ODY1NgA - + 3 MC41AA - + 2 15 - + Helvetica-Bold 15 16 + + + 292 + {{40, 67}, {113, 44}} + + + _NS:9 + NO + IBIPadFramework + 0 + 0 + 1 + Clear iCloud + + + 1 + MC4xOTYwNzg0MzQ2IDAuMzA5ODAzOTMyOSAwLjUyMTU2ODY1NgA + + + + + {320, 130} - _NS:9 @@ -203,11 +233,11 @@ - clearButton + storeLoadingActivity - + - 14 + 16 @@ -234,14 +264,23 @@ 11 + + + rebuildiCloud: + + + 7 + + 20 + cleariCloud: - + 7 - 13 + 19 @@ -274,7 +313,9 @@ - + + + Table Header View @@ -290,8 +331,18 @@ - 12 - + 15 + + + + + 17 + + + + + 18 + @@ -301,7 +352,9 @@ com.apple.InterfaceBuilder.IBCocoaTouchPlugin UIResponder com.apple.InterfaceBuilder.IBCocoaTouchPlugin - com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin com.apple.InterfaceBuilder.IBCocoaTouchPlugin com.apple.InterfaceBuilder.IBCocoaTouchPlugin com.apple.InterfaceBuilder.IBCocoaTouchPlugin @@ -311,61 +364,17 @@ - 14 - - - - - MasterViewController - UITableViewController - - id - id - - - - cleariCloud: - id - - - setiCloudState: - id - - - - UIButton - UISwitch - UIView - - - - clearButton - UIButton - - - iCloudSwitch - UISwitch - - - tableHeaderView - UIView - - - - IBProjectSource - ./Classes/MasterViewController.h - - - + 20 + 0 IBIPadFramework com.apple.InterfaceBuilder.CocoaTouchPlugin.iPhoneOS - + YES 3 - 1179 + 2083 diff --git a/iCloudStoreManagerExample/en.lproj/MasterViewController_iPhone.xib b/UbiquityStoreManagerExample/en.lproj/MasterViewController_iPhone.xib similarity index 74% rename from iCloudStoreManagerExample/en.lproj/MasterViewController_iPhone.xib rename to UbiquityStoreManagerExample/en.lproj/MasterViewController_iPhone.xib index 3bff54c..1a085a0 100644 --- a/iCloudStoreManagerExample/en.lproj/MasterViewController_iPhone.xib +++ b/UbiquityStoreManagerExample/en.lproj/MasterViewController_iPhone.xib @@ -1,22 +1,23 @@ - 1296 - 11D50 - 2182 - 1138.32 - 568.00 + 1552 + 12C60 + 3084 + 1187.34 + 625.00 com.apple.InterfaceBuilder.IBCocoaTouchPlugin - 1179 + 2083 + IBProxyObject + IBUIActivityIndicatorView IBUIButton - IBUITableView + IBUILabel IBUISwitch + IBUITableView IBUIView - IBUILabel - IBProxyObject com.apple.InterfaceBuilder.IBCocoaTouchPlugin @@ -62,10 +63,10 @@ 292 - {{146, 20}, {94, 27}} + {{156, 20}, {94, 27}} - + _NS:9 NO IBCocoaTouchFramework @@ -76,7 +77,7 @@ 292 - {{81, 22}, {129, 24}} + {{24, 22}, {129, 24}} @@ -90,10 +91,12 @@ 1 MCAwIDAAA + darkTextColor 0 10 + 2 2 20 @@ -104,10 +107,10 @@ 16 - + 292 - {{81, 73}, {159, 37}} + {{171, 67}, {130, 44}} _NS:9 @@ -116,26 +119,61 @@ 0 0 1 - Clear iCloud Data + Rebuild iCloud 1 MC4xOTYwNzg0MzQ2IDAuMzA5ODAzOTMyOSAwLjUyMTU2ODY1NgA - + 3 MC41AA - + 2 15 - + Helvetica-Bold 15 16 + + + 292 + {{40, 67}, {113, 44}} + + + + _NS:9 + NO + IBCocoaTouchFramework + 0 + 0 + 1 + Clear iCloud + + + 1 + MC4xOTYwNzg0MzQ2IDAuMzA5ODAzOTMyOSAwLjUyMTU2ODY1NgA + + + + + + + + -2147483356 + {{256, 24}, {20, 20}} + + + + _NS:9 + NO + IBCocoaTouchFramework + 2 + {320, 130} @@ -180,11 +218,11 @@ - clearButton + storeLoadingActivity - + - 14 + 16 @@ -211,14 +249,23 @@ 11 + + + rebuildiCloud: + + + 7 + + 23 + cleariCloud: - + 7 - 13 + 22 @@ -249,9 +296,11 @@ 6 - + + + Table Header View @@ -267,8 +316,18 @@ - 12 - + 15 + + + + + 17 + + + + + 21 + @@ -278,8 +337,10 @@ com.apple.InterfaceBuilder.IBCocoaTouchPlugin UIResponder com.apple.InterfaceBuilder.IBCocoaTouchPlugin - com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin com.apple.InterfaceBuilder.IBCocoaTouchPlugin com.apple.InterfaceBuilder.IBCocoaTouchPlugin com.apple.InterfaceBuilder.IBCocoaTouchPlugin @@ -288,7 +349,7 @@ - 14 + 23 @@ -297,6 +358,7 @@ UITableViewController id + id id @@ -304,6 +366,10 @@ cleariCloud: id + + rebuildiCloud: + id + setiCloudState: id @@ -312,6 +378,7 @@ UIButton UISwitch + UIActivityIndicatorView UIView @@ -323,6 +390,10 @@ iCloudSwitch UISwitch + + storeLoadingActivity + UIActivityIndicatorView + tableHeaderView UIView @@ -339,10 +410,10 @@ IBCocoaTouchFramework com.apple.InterfaceBuilder.CocoaTouchPlugin.iPhoneOS - + YES 3 - 1179 + 2083 diff --git a/iCloudStoreManagerExample/main.m b/UbiquityStoreManagerExample/main.m similarity index 90% rename from iCloudStoreManagerExample/main.m rename to UbiquityStoreManagerExample/main.m index e24eb06..e84d58a 100644 --- a/iCloudStoreManagerExample/main.m +++ b/UbiquityStoreManagerExample/main.m @@ -1,6 +1,6 @@ // // main.m -// iCloudStoreManagerExample +// UbiquityStoreManagerExample // // Created by Aleksey Novicov on 3/27/12. // Copyright (c) 2012 Yodel Code LLC. All rights reserved. diff --git a/iCloudStoreManager/UbiquityStoreManager.h b/iCloudStoreManager/UbiquityStoreManager.h deleted file mode 100644 index 41d77fc..0000000 --- a/iCloudStoreManager/UbiquityStoreManager.h +++ /dev/null @@ -1,104 +0,0 @@ -// -// UbiquityStoreManager.h -// UbiquityStoreManager -// -// Created by Aleksey Novicov on 3/27/12. -// Copyright (c) 2012 Yodel Code LLC. All rights reserved. -// -// UbiquityStoreManager manages the transfer of your SQL CoreData store from your local -// application sandbox to iCloud. Even though it is not reinforced, UbiquityStoreManager -// is expected to be used as a singleton. This sample code is curently for iOS only. -// -// This class implements a very simple model. Once iCloud is seeded by data from a -// particular device, the iCloud store can never be re-seeded with fresh data. -// However, different devices can repeatedly switch between using their local store -// and the iCloud store. This is not necessarily a recommended practice but is implemented -// this way for testing and learning purposes. -// -// NSUbiquitousKeyValueStore is the mechanism used to discover which iCloud store to use. -// There may be better ways, but for now, that is what is being used. -// -// Use the "Clear iCloud Data" button to reset iCloud data. This hard reset will propagate to all -// devices if the device's app is running. However, there may be a propagation delay of 20 sec. or more. -// or more. - -#import -#import -#if TARGET_OS_IPHONE -#import -#else -#import -#endif - -extern NSString * const RefetchAllDatabaseDataNotificationKey; -extern NSString * const RefreshAllViewsNotificationKey; - -typedef enum { - UbiquityStoreManagerErrorCauseDeleteStore, - UbiquityStoreManagerErrorCauseDeleteLogs, - UbiquityStoreManagerErrorCauseCreateStorePath, - UbiquityStoreManagerErrorCauseClearStore, - UbiquityStoreManagerErrorCauseOpenLocalStore, - UbiquityStoreManagerErrorCauseOpenCloudStore, -} UbiquityStoreManagerErrorCause; - -@class UbiquityStoreManager; - -@protocol UbiquityStoreManagerDelegate -- (NSManagedObjectContext *)managedObjectContextForUbiquityStoreManager:(UbiquityStoreManager *)usm; -@optional -- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didSwitchToiCloud:(BOOL)didSwitch; -- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager log:(NSString *)message; -- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didEncounterError:(NSError *)error cause:(UbiquityStoreManagerErrorCause)cause context:(id)context; -@end - -#if TARGET_OS_IPHONE -@interface UbiquityStoreManager : NSObject -#else -@interface UbiquityStoreManager : NSObject -#endif - -// The delegate confirms when a device has been switched to using either iCloud data or local data -@property (nonatomic, weak) id delegate; - -// This property indicates whether the iCloud store or the local store is in use. To -// change state of this property, use useiCloudStore: method -@property (nonatomic, readonly) BOOL iCloudEnabled; - -// This property indicates when the persistentStoreCoordinator is ready. This property -// is always set immediately before the RefetchAllDatabaseDataNotification is sent. -@property (nonatomic, readonly) BOOL isReady; - -// Setting this property to YES is helpful for test purposes. It is highly recommended -// to set this to NO for production deployment -@property (nonatomic) BOOL hardResetEnabled; - -// Start by instantiating a UbiquityStoreManager with a managed object model. A valid localStoreURL -// is also required even if iCloud support is not currently enabled for this device. If it is enabled, -// it is required in case the user disables iCloud support for this device. If iCloud support is disabled -// after being initially enabled, the store on iCloud is NOT migrated back to the local device. -- (id)initWithManagedObjectModel:(NSManagedObjectModel *)model localStoreURL:(NSURL *)storeURL - containerIdentifier:(NSString *)containerIdentifier additionalStoreOptions:(NSDictionary *)additionalStoreOptions; - -// Always use this method to instantiate or retrieve the main persistentStoreCoordinator. -- (NSPersistentStoreCoordinator *)persistentStoreCoordinator; - -// If the user has decided to start using iCloud, call this method. And vice versa. -- (void)useiCloudStore:(BOOL)willUseiCloud alertUser:(BOOL)alertUser; - -// Reset iCloud data. Intended for test purposes only -- (void)hardResetCloudStorage; -- (void)hardResetLocalStorage; - -// Checks iCloud to ensure user has not deleted all iCloud data (nuke all use case). -// If the iCloud data has been deleted from within the Settings app or Mac System Preferences, -// iCloud will be disabled and the active store will be switched over to local store -- (void)checkiCloudStatus; - -// Array of all files and directorys in the ubiquity store. Useful for testing -- (NSArray *)fileList; - -// File URL for the currently selected store -- (NSURL *)currentStoreURL; - -@end diff --git a/iCloudStoreManager/UbiquityStoreManager.m b/iCloudStoreManager/UbiquityStoreManager.m deleted file mode 100644 index 8cafba2..0000000 --- a/iCloudStoreManager/UbiquityStoreManager.m +++ /dev/null @@ -1,787 +0,0 @@ -// -// UbiquityStoreManager.m -// UbiquityStoreManager -// -// Created by Aleksey Novicov on 3/27/12. -// Copyright (c) 2012 Yodel Code LLC. All rights reserved. -// - -#import "UbiquityStoreManager.h" - -#if TARGET_OS_IPHONE -#define OS_Alert UIAlertView -#else -#define OS_Alert NSAlert -#endif - -NSString * const RefetchAllDatabaseDataNotificationKey = @"RefetchAllDatabaseData"; -NSString * const RefreshAllViewsNotificationKey = @"RefreshAllViews"; - -NSString *LocalUUIDKey = @"LocalUUIDKey"; -NSString *iCloudUUIDKey = @"iCloudUUIDKey"; -NSString *iCloudEnabledKey = @"iCloudEnabledKey"; -NSString *DatabaseDirectoryName = @"Database.nosync"; -NSString *DataDirectoryName = @"Data"; - -@interface UbiquityStoreManager () { - NSDictionary *additionalStoreOptions__; - NSString *containerIdentifier__; - NSManagedObjectModel *model__; - NSPersistentStoreCoordinator *persistentStoreCoordinator__; - NSPersistentStore *persistentStore__; - NSURL *localStoreURL__; - OS_Alert *moveDataAlert; - OS_Alert *switchToiCloudAlert; - OS_Alert *switchToLocalAlert; - dispatch_queue_t persistentStorageQueue; -} - -@property (nonatomic) NSString *localUUID; -@property (nonatomic) NSString *iCloudUUID; - -- (NSString *)freshUUID; -- (void)registerForNotifications; -- (void)removeNotifications; -- (void)migrate:(BOOL)migrate andUseCloudStorageWithUUID:(NSString *)uuid completionBlock:(void (^)(BOOL usingiCloud))completionBlock; -- (void)checkiCloudStatus; -@end - - -@implementation UbiquityStoreManager - -@synthesize delegate; -@synthesize isReady = _isReady; -@synthesize hardResetEnabled = _hardResetEnabled; - -- (id)initWithManagedObjectModel:(NSManagedObjectModel *)model localStoreURL:(NSURL *)storeURL - containerIdentifier:(NSString *)containerIdentifier additionalStoreOptions:(NSDictionary *)additionalStoreOptions { - self = [super init]; - if (self) { - additionalStoreOptions__ = additionalStoreOptions; - containerIdentifier__ = containerIdentifier; - model__ = model; - localStoreURL__ = storeURL; - persistentStorageQueue = dispatch_queue_create([@"PersistentStorageQueue" UTF8String], DISPATCH_QUEUE_SERIAL); - - // Start iCloud connection - [self updateLocalCopyOfiCloudUUID]; - - [self checkiCloudStatus]; - [self registerForNotifications]; - } - - return self; -} - -- (void)dealloc { - [self removeNotifications]; - dispatch_release(persistentStorageQueue); -} - -#pragma mark - File Handling - -- (NSURL *)iCloudStoreURLForUUID:(NSString *)uuid { - NSFileManager *fileManager = [NSFileManager defaultManager]; - NSURL *cloudURL = [fileManager URLForUbiquityContainerIdentifier:containerIdentifier__]; - NSString *databaseContent = [[cloudURL path] stringByAppendingPathComponent:DatabaseDirectoryName]; - NSString *storePath = [databaseContent stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.sqlite", uuid]]; - - return [NSURL fileURLWithPath:storePath]; -} - -- (void)deleteStoreForUUID:(NSString *) uuid { - // TODO: -} - -- (void)deleteStoreDirectory { - NSFileManager *fileManager = [NSFileManager defaultManager]; - NSURL *cloudURL = [fileManager URLForUbiquityContainerIdentifier:containerIdentifier__]; - NSString *databaseContent = [[cloudURL path] stringByAppendingPathComponent:DatabaseDirectoryName]; - - if ([fileManager fileExistsAtPath:databaseContent]) { - NSError *error = nil; - [fileManager removeItemAtPath:databaseContent error:&error]; - - if (error) - if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didEncounterError:cause:context:)]) - [self.delegate ubiquityStoreManager:self didEncounterError:error cause:UbiquityStoreManagerErrorCauseDeleteStore context:databaseContent]; - else - NSLog(@"Error deleting old store: %@", error); - } -} - -- (void)createStoreDirectoryIfNecessary { - NSFileManager *fileManager = [NSFileManager defaultManager]; - NSURL *cloudURL = [fileManager URLForUbiquityContainerIdentifier:containerIdentifier__]; - NSString *databaseContent = [[cloudURL path] stringByAppendingPathComponent:DatabaseDirectoryName]; - - if (![fileManager fileExistsAtPath:databaseContent]) { - NSError *error = nil; - [fileManager createDirectoryAtPath:databaseContent withIntermediateDirectories:YES attributes:nil error:&error]; - - if (error) - if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didEncounterError:cause:context:)]) - [self.delegate ubiquityStoreManager:self didEncounterError:error cause:UbiquityStoreManagerErrorCauseCreateStorePath context:databaseContent]; - else - NSLog(@"Error creating database directory: %@", error); - } -} - -- (NSURL *)transactionLogsURL { - NSFileManager *fileManager = [NSFileManager defaultManager]; - NSURL *cloudURL = [fileManager URLForUbiquityContainerIdentifier:containerIdentifier__]; - NSString* coreDataCloudContent = [[cloudURL path] stringByAppendingPathComponent:DataDirectoryName]; - - return [NSURL fileURLWithPath:coreDataCloudContent]; -} - -- (void)deleteTransactionLogs { - NSError *error = nil; - NSString *path = [[self transactionLogsURL] path]; - NSFileManager *fileManager = [NSFileManager defaultManager]; - [fileManager removeItemAtPath:path error:&error]; - if (error) - if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didEncounterError:cause:context:)]) - [self.delegate ubiquityStoreManager:self didEncounterError:error cause:UbiquityStoreManagerErrorCauseDeleteLogs context:path]; - else - NSLog(@"Error deleting local store: %@", error); - -} - -- (NSURL *)transactionLogsURLForUUID:(NSString *)uuid { - return [[self transactionLogsURL] URLByAppendingPathComponent:uuid isDirectory:YES]; -} - -- (void)deleteTransactionLogsForUUID:(NSString *)uuid { - if (uuid) { - NSFileManager *fileManager = [NSFileManager defaultManager]; - NSURL *cloudURL = [fileManager URLForUbiquityContainerIdentifier:containerIdentifier__]; - - // Can only continue if iCloud is available - if (cloudURL) { - NSError *error = nil; - NSString *transactionLogsForUUID = [[self transactionLogsURLForUUID:uuid] path]; - [fileManager removeItemAtPath:transactionLogsForUUID error:&error]; - if (error) - if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didEncounterError:cause:context:)]) - [self.delegate ubiquityStoreManager:self didEncounterError:error cause:UbiquityStoreManagerErrorCauseDeleteLogs context:transactionLogsForUUID]; - else - NSLog(@"Error deleting local store: %@", error); - } - } -} - -#pragma mark - Message Strings - -// Subclass UbiquityStoreManager and override these methods if you want to customize these messages - -- (NSString *)moveDataToiCloudTitle { - return @"Move Data to iCloud"; -} - -- (NSString *)moveDataToiCloudMessage { - return @"Your data is about to be moved to iCloud. If you prefer to start using iCloud with data from a different device, tap Cancel and enable iCloud from that other device."; -} - -- (NSString *)switchDataToiCloudTitle { - return @"iCloud Data"; -} - -- (NSString *)switchDataToiCloudMessage { - return @"Would you like to switch to using data from iCloud?"; -} - -- (NSString *)tryLaterTitle { - return @"iCloud Not Available"; -} - -- (NSString *)tryLaterMessage { - return @"iCloud is not currently available. Please try again later."; -} - - -- (NSString *)switchToLocalDataTitle { - return @"Stop Using iCloud"; -} - -- (NSString *)switchToLocalDataMessage { - return @"If you stop using iCloud you will switch to using local data on this device only. Your local data is completely separate from iCloud. Any changes you make will not be be synchronized with iCloud."; -} - -#pragma mark - UIAlertView - -- (void)alertView:(OS_Alert *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex { - if (alertView == moveDataAlert) { - if (buttonIndex == 1) { - // Move the data from the local store to the iCloud store - [self setupCloudStorageWithUUID:[self freshUUID]]; - } - else { - [self didSwitchToiCloud:NO]; - } - } - - if (alertView == switchToiCloudAlert) { - if (buttonIndex == 1) { - // Switch to using data from iCloud - [self useCloudStorage]; - } - else { - [self didSwitchToiCloud:NO]; - } - } - - if (alertView == switchToLocalAlert) { - if (buttonIndex == 1) { - // Switch to using data from iCloud - [self useLocalStorage]; - } - else { - [self didSwitchToiCloud:YES]; - } - } -} - -- (void)moveDataToiCloudAlert { -#if TARGET_OS_IPHONE - moveDataAlert = [[UIAlertView alloc] initWithTitle: [self moveDataToiCloudTitle] - message: [self moveDataToiCloudMessage] - delegate: self - cancelButtonTitle: @"Cancel" - otherButtonTitles: @"Move Data", nil]; - [moveDataAlert show]; -#else - moveDataAlert = [NSAlert alertWithMessageText:[self moveDataToiCloudTitle] - defaultButton:@"Move Data" - alternateButton:@"Cancel" - otherButton:nil - informativeTextWithFormat:[self moveDataToiCloudMessage]]; - NSInteger button = [moveDataAlert runModal]; - [self alertView:moveDataAlert didDismissWithButtonIndex:button == NSAlertDefaultReturn? 1: 0]; -#endif -} - -- (void)switchToiCloudDataAlert { -#if TARGET_OS_IPHONE - switchToiCloudAlert = [[UIAlertView alloc] initWithTitle: [self switchDataToiCloudTitle] - message: [self switchDataToiCloudMessage] - delegate: self - cancelButtonTitle: @"Cancel" - otherButtonTitles: @"Use iCloud", nil]; - [switchToiCloudAlert show]; -#else - switchToiCloudAlert = [NSAlert alertWithMessageText:[self switchDataToiCloudTitle] - defaultButton:@"Use iCloud" - alternateButton:@"Cancel" - otherButton:nil - informativeTextWithFormat:[self switchDataToiCloudMessage]]; - NSInteger button = [switchToiCloudAlert runModal]; - [self alertView:switchToiCloudAlert didDismissWithButtonIndex:button == NSAlertDefaultReturn? 1: 0]; -#endif -} - -- (void)tryLaterAlert { -#if TARGET_OS_IPHONE - UIAlertView *alert = [[UIAlertView alloc] initWithTitle: [self tryLaterTitle] - message: [self tryLaterMessage] - delegate: nil - cancelButtonTitle: @"Done" - otherButtonTitles: nil]; - [alert show]; -#else - NSAlert *alert = [NSAlert alertWithMessageText:[self tryLaterTitle] - defaultButton:@"Cancel" - alternateButton:nil - otherButton:nil - informativeTextWithFormat:[self tryLaterMessage]]; - [alert runModal]; -#endif -} - -- (void)switchToLocalDataAlert { -#if TARGET_OS_IPHONE - switchToLocalAlert = [[UIAlertView alloc] initWithTitle: [self switchToLocalDataTitle] - message: [self switchToLocalDataMessage] - delegate: self - cancelButtonTitle: @"Cancel" - otherButtonTitles: @"Continue", nil]; - [switchToLocalAlert show]; -#else - switchToLocalAlert = [NSAlert alertWithMessageText:[self switchToLocalDataTitle] - defaultButton:@"Continue" - alternateButton:@"Cancel" - otherButton:nil - informativeTextWithFormat:[self switchToLocalDataMessage]]; - NSInteger button = [switchToLocalAlert runModal]; - [self alertView:switchToLocalAlert didDismissWithButtonIndex:button == NSAlertDefaultReturn? 1: 0]; -#endif -} - -#pragma mark - Test Methods - -- (void)hardResetLocalStorage { - if (_hardResetEnabled) { - NSError *error; - [[NSFileManager defaultManager] removeItemAtURL:localStoreURL__ error:&error]; - if (error) - if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didEncounterError:cause:context:)]) - [self.delegate ubiquityStoreManager:self didEncounterError:error cause:UbiquityStoreManagerErrorCauseDeleteStore context:localStoreURL__]; - else - NSLog(@"Error deleting local store: %@", error); - } -} - -- (void)hardResetCloudStorage { - if (_hardResetEnabled) { - [self migrate:NO andUseCloudStorageWithUUID:nil completionBlock:^(BOOL usingiCloud) { - [self deleteStoreDirectory]; - [self deleteTransactionLogs]; - - // Setting iCloudUUID to nil will propagate to all other devices, - // and automatically force them to switch over to their local stores - - self.iCloudUUID = nil; - self.localUUID = nil; - self.iCloudEnabled = NO; - }]; - } -} - -- (NSArray *)fileList { - NSArray *fileList = nil; - - NSFileManager *fileManager = [NSFileManager defaultManager]; - NSURL *cloudURL = [fileManager URLForUbiquityContainerIdentifier:containerIdentifier__]; - - if (cloudURL) - fileList = [fileManager subpathsAtPath:[cloudURL path]]; - - return fileList; -} - -#pragma mark - Persistent Store Management - -- (void)clearPersistentStore { - if (persistentStore__) { - NSError *error = nil; - [persistentStoreCoordinator__ removePersistentStore:persistentStore__ error:&error]; - persistentStore__ = nil; - - if (error) - if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didEncounterError:cause:context:)]) - [self.delegate ubiquityStoreManager:self didEncounterError:error cause:UbiquityStoreManagerErrorCauseClearStore context:persistentStore__]; - else - NSLog(@"Error removing persistent store: %@", error); - } -} - -- (NSPersistentStoreCoordinator *)persistentStoreCoordinator { - - if (persistentStoreCoordinator__ == nil) { - persistentStoreCoordinator__ = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: model__]; - - NSString *uuid = (self.iCloudEnabled) ? self.localUUID : nil; - [self migrate:NO andUseCloudStorageWithUUID:uuid completionBlock:nil]; - } - return persistentStoreCoordinator__; -} - -- (void)migrateToiCloud:(BOOL)migrate persistentStoreCoordinator:(NSPersistentStoreCoordinator *)psc with:(NSString *)uuid { - NSMutableDictionary *options; - - NSAssert([[psc persistentStores] count] == 0, @"There were more persistent stores than expected"); - - [self createStoreDirectoryIfNecessary]; - - NSError *error = nil; - NSURL *transactionLogsURL = [self transactionLogsURLForUUID:uuid]; - - options = [NSMutableDictionary dictionaryWithObjectsAndKeys: - uuid, NSPersistentStoreUbiquitousContentNameKey, - transactionLogsURL, NSPersistentStoreUbiquitousContentURLKey, - [NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption, - [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, - nil]; - [options addEntriesFromDictionary:additionalStoreOptions__]; - - [psc lock]; - - NSURL *cloudStoreURL = [self iCloudStoreURLForUUID:uuid]; - if (migrate) { - // Clear old registered notifcations. This was required to address an exception that occurs when using - // a persistent store on iCloud setup by another device (Object's persistent store is not reachable - // from this NSManagedObjectContext's coordinator) - [[NSNotificationCenter defaultCenter] removeObserver: self - name: NSPersistentStoreDidImportUbiquitousContentChangesNotification - object: psc]; - - // Add the store to migrate - NSPersistentStore * migratedStore = [psc addPersistentStoreWithType: NSSQLiteStoreType - configuration: nil - URL: localStoreURL__ - options: additionalStoreOptions__ - error: &error]; - - if (error) - if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didEncounterError:cause:context:)]) - [self.delegate ubiquityStoreManager:self didEncounterError:error cause:UbiquityStoreManagerErrorCauseOpenLocalStore context:localStoreURL__]; - else - NSLog(@"Prepping migrated store error: %@", error); - - error = nil; - persistentStore__ = [psc migratePersistentStore: migratedStore - toURL: cloudStoreURL - options: options - withType: NSSQLiteStoreType - error: &error]; - - [[NSNotificationCenter defaultCenter]addObserver: self - selector: @selector(mergeChanges:) - name: NSPersistentStoreDidImportUbiquitousContentChangesNotification - object: psc]; - } - else { - persistentStore__ = [psc addPersistentStoreWithType: NSSQLiteStoreType - configuration: nil - URL: cloudStoreURL - options: options - error: &error]; - } - [psc unlock]; - - if (error) - if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didEncounterError:cause:context:)]) - [self.delegate ubiquityStoreManager:self didEncounterError:error cause:UbiquityStoreManagerErrorCauseOpenCloudStore context:cloudStoreURL]; - else { - NSLog(@"Persistent store error: %@", error); - NSAssert([[psc persistentStores] count] == 1, @"Not the expected number of persistent stores"); - } -} - -- (void)migrate:(BOOL)migrate andUseCloudStorageWithUUID:(NSString *)uuid completionBlock:(void (^)(BOOL usingiCloud))completionBlock { - - if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:log:)]) - [self.delegate ubiquityStoreManager:self log:[NSString stringWithFormat:@"Setting up store with UUID: %@", uuid]]; - else - NSLog(@"Setting up store with UUID: %@", uuid); - BOOL willUseiCloud = (uuid != nil); - - // TODO: Check for use case where user checks out of one iCloud account, and logs into another! - - // TODO: Test deletion from Settings App -> Manage Data -> Delete Data (nuke option) - - NSPersistentStoreCoordinator* psc = persistentStoreCoordinator__; - - // Do this asynchronously since if this is the first time this particular device is syncing with preexisting - // iCloud content it may take a long long time to download - dispatch_async(persistentStorageQueue, ^{ - NSFileManager *fileManager = [NSFileManager defaultManager]; - NSMutableDictionary *options; - - // Clear previous persistentStore - [self clearPersistentStore]; - - NSURL *cloudURL = [fileManager URLForUbiquityContainerIdentifier:containerIdentifier__]; - NSString* coreDataCloudContent = [[cloudURL path] stringByAppendingPathComponent:DataDirectoryName]; - - BOOL usingiCloud = ([coreDataCloudContent length] != 0) && willUseiCloud; - - if (usingiCloud) { - // iCloud is available - [self migrateToiCloud:migrate persistentStoreCoordinator:psc with:uuid]; - } - else { - // iCloud is not available - options = [NSMutableDictionary dictionaryWithObjectsAndKeys: - [NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption, - [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, - nil]; - [options addEntriesFromDictionary:additionalStoreOptions__]; - - [psc lock]; - - NSError *error = nil; - persistentStore__ = [psc addPersistentStoreWithType: NSSQLiteStoreType - configuration: nil - URL: localStoreURL__ - options: options - error: &error]; - if (error) - if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didEncounterError:cause:context:)]) - [self.delegate ubiquityStoreManager:self didEncounterError:error cause:UbiquityStoreManagerErrorCauseOpenLocalStore context:localStoreURL__]; - else - NSLog(@"Persistent store error: %@", error); - - [psc unlock]; - } - - if (![[psc persistentStores] count]) - return; - - _isReady = YES; - - NSAssert([[psc persistentStores] count] == 1, @"Not the expected number of persistent stores"); - - NSString *usingiCloudString = (usingiCloud) ? @" using iCloud!" : @"!"; - - if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:log:)]) - [self.delegate ubiquityStoreManager:self log:[NSString stringWithFormat:@"Asynchronously added persistent store%@", usingiCloudString]]; - else - NSLog(@"Asynchronously added persistent store%@", usingiCloudString); - - if (completionBlock) { - completionBlock(usingiCloud); - } - - dispatch_async(dispatch_get_main_queue(), ^{ - [[NSNotificationCenter defaultCenter] postNotificationName:RefetchAllDatabaseDataNotificationKey object:self userInfo:nil]; - [self didSwitchToiCloud:willUseiCloud]; - }); - }); -} - -- (void)useCloudStorage { - if (persistentStoreCoordinator__ == nil) - persistentStoreCoordinator__ = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: model__]; - - [self migrate:NO andUseCloudStorageWithUUID:self.iCloudUUID completionBlock:^(BOOL usingiCloud) { - if (usingiCloud) { - self.localUUID = self.iCloudUUID; - self.iCloudEnabled = YES; - } - }]; -} - -- (void)useLocalStorage { - if (persistentStoreCoordinator__ == nil) - persistentStoreCoordinator__ = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: model__]; - - [self migrate:NO andUseCloudStorageWithUUID:nil completionBlock:^(BOOL usingiCloud) { - self.localUUID = nil; - self.iCloudEnabled = NO; - }]; -} - -- (void)setupCloudStorageWithUUID:(NSString *)uuid { - - if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:log:)]) - [self.delegate ubiquityStoreManager:self log:[NSString stringWithFormat:@"Setting up iCloud data with new UUID: %@", uuid]]; - else - NSLog(@"Setting up iCloud data with new UUID: %@", uuid); - - [self migrate:YES andUseCloudStorageWithUUID:uuid completionBlock:^(BOOL usingiCloud) { - if (usingiCloud) { - self.localUUID = uuid; - self.iCloudUUID = uuid; - self.iCloudEnabled = YES; - } - }]; -} - -- (void)replaceiCloudStoreWithUUID:(NSString *)uuid { - if (persistentStoreCoordinator__ == nil) - persistentStoreCoordinator__ = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: model__]; - - [self migrate:NO andUseCloudStorageWithUUID:uuid completionBlock:^(BOOL usingiCloud) { - if (usingiCloud) { - self.localUUID = uuid; - self.iCloudEnabled = YES; - } - else { - if (_hardResetEnabled) { - // Hard reset has occurred. Delete database and transaction logs - [self deleteStoreDirectory]; - [self deleteTransactionLogs]; - } - - self.localUUID = nil; - self.iCloudEnabled = NO; - } - }]; -} - -- (NSURL *)currentStoreURL { - if (self.iCloudEnabled) - return [self iCloudStoreURLForUUID:self.iCloudUUID]; - else - return localStoreURL__; -} - -#pragma mark - Top Level Methods - -- (void)checkiCloudStatus { - if (self.iCloudEnabled) { - NSFileManager *fileManager = [NSFileManager defaultManager]; - NSURL *cloudURL = [fileManager URLForUbiquityContainerIdentifier:containerIdentifier__]; - - // If we have only one file/directory (Documents directory), then iCloud data has been deleted by user - if ((cloudURL == nil) || [[self fileList] count] < 2) - [self useLocalStorage]; - } -} - -- (void)useiCloudStore:(BOOL)willUseiCloud alertUser:(BOOL)alertUser { - // To provide the option of using iCloud immediately upon first running of an app, - // make sure a persistentStoreCoordinator exists. - - if (persistentStoreCoordinator__ == nil) - persistentStoreCoordinator__ = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: model__]; - - if (willUseiCloud) { - if (!self.iCloudEnabled) { - NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; - - // If an iCloud store already exists, ask the user if they want to switch over to iCloud - if (cloud) { - if (self.iCloudUUID) { - if (alertUser && [localStoreURL__ checkResourceIsReachableAndReturnError:nil]) - [self switchToiCloudDataAlert]; - else - [self useCloudStorage]; - } - else { - if (alertUser && [localStoreURL__ checkResourceIsReachableAndReturnError:nil]) - [self moveDataToiCloudAlert]; - else - [self setupCloudStorageWithUUID:[self freshUUID]]; - } - } - else if (alertUser) { - [self tryLaterAlert]; - } - } - } - else { - if (self.iCloudEnabled) { - if (alertUser && [localStoreURL__ checkResourceIsReachableAndReturnError:nil]) - [self switchToLocalDataAlert]; - else - [self useLocalStorage]; - } - } -} - -- (void)didSwitchToiCloud:(BOOL)didSwitch { - if ([delegate respondsToSelector:@selector(ubiquityStoreManager:didSwitchToiCloud:)]) { - [delegate ubiquityStoreManager:self didSwitchToiCloud:didSwitch]; - } -} - -#pragma mark - Properties - -- (BOOL)iCloudEnabled { - NSUserDefaults *local = [NSUserDefaults standardUserDefaults]; - return [local boolForKey:iCloudEnabledKey]; -} - -- (void)setICloudEnabled:(BOOL)enabled { - NSUserDefaults *local = [NSUserDefaults standardUserDefaults]; - [local setBool:enabled forKey:iCloudEnabledKey]; -} - -- (NSString *)freshUUID { - CFUUIDRef uuidRef = CFUUIDCreate(kCFAllocatorDefault); - CFStringRef uuidStringRef = CFUUIDCreateString(kCFAllocatorDefault, uuidRef); - - CFRelease(uuidRef); - - return (__bridge_transfer NSString *)uuidStringRef; -} - -- (void)setLocalUUID:(NSString *)uuid { - NSUserDefaults *local = [NSUserDefaults standardUserDefaults]; - [local setObject:uuid forKey:LocalUUIDKey]; - [local synchronize]; -} - -- (NSString *)localUUID { - NSUserDefaults *local = [NSUserDefaults standardUserDefaults]; - return [local objectForKey:LocalUUIDKey]; -} - -- (void)setICloudUUID:(NSString *)uuid { - NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; - [cloud setObject:uuid forKey:iCloudUUIDKey]; - [cloud synchronize]; - - NSUserDefaults *local = [NSUserDefaults standardUserDefaults]; - [local setObject:uuid forKey:iCloudUUIDKey]; - [local synchronize]; -} - -- (NSString *)iCloudUUID { - NSUserDefaults *local = [NSUserDefaults standardUserDefaults]; - return [local objectForKey:iCloudUUIDKey]; -} - -- (void)updateLocalCopyOfiCloudUUID { - NSUserDefaults *local = [NSUserDefaults standardUserDefaults]; - NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; - [local setObject:[cloud objectForKey:iCloudUUIDKey] forKey:iCloudUUIDKey]; - [local synchronize]; -} - -#pragma mark - KeyValueStore Notification - -- (void)keyValueStoreChanged:(NSNotification *)note { - - if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:log:)]) - [self.delegate ubiquityStoreManager:self log:[NSString stringWithFormat:@"KeyValueStore changed: %@", [note.userInfo objectForKey:NSUbiquitousKeyValueStoreChangedKeysKey]]]; - else - NSLog(@"KeyValueStore changed: %@", [note.userInfo objectForKey:NSUbiquitousKeyValueStoreChangedKeysKey]); - - NSDictionary* changedKeys = [note.userInfo objectForKey:NSUbiquitousKeyValueStoreChangedKeysKey]; - for (NSString *key in changedKeys) { - if ([key isEqualToString:iCloudUUIDKey]) { - - // Latest change wins - [self updateLocalCopyOfiCloudUUID]; - [self replaceiCloudStoreWithUUID:self.iCloudUUID]; - } - } -} - -#pragma mark - Notifications - -- (void)mergeChanges:(NSNotification *)note { - - if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:log:)]) - [self.delegate ubiquityStoreManager:self log:[NSString stringWithFormat:@"Ubiquitous store changes: %@", note.userInfo]]; - else - NSLog(@"Ubiquitous store changes: %@", note.userInfo); - - dispatch_async(persistentStorageQueue, ^{ - NSManagedObjectContext *moc = [self.delegate managedObjectContextForUbiquityStoreManager:self]; - [moc performBlockAndWait:^{ - [moc mergeChangesFromContextDidSaveNotification:note]; - }]; - - dispatch_async(dispatch_get_main_queue(), ^{ - NSNotification* refreshNotification = [NSNotification notificationWithName: RefreshAllViewsNotificationKey - object: self - userInfo: [note userInfo]]; - - [[NSNotificationCenter defaultCenter] postNotification:refreshNotification]; - }); - }); -} - - -- (void)registerForNotifications { - [[NSNotificationCenter defaultCenter] addObserver: self - selector: @selector(keyValueStoreChanged:) - name: NSUbiquitousKeyValueStoreDidChangeExternallyNotification - object: nil]; - - [[NSNotificationCenter defaultCenter]addObserver: self - selector: @selector(mergeChanges:) - name: NSPersistentStoreDidImportUbiquitousContentChangesNotification - object: [self persistentStoreCoordinator]]; - -} - -- (void)removeNotifications { - [[NSNotificationCenter defaultCenter] removeObserver:self]; -} - -@end diff --git a/iCloudStoreManagerExample/iCloudStoreManagerExample.entitlements b/iCloudStoreManagerExample/iCloudStoreManagerExample.entitlements deleted file mode 100644 index fe995ce..0000000 --- a/iCloudStoreManagerExample/iCloudStoreManagerExample.entitlements +++ /dev/null @@ -1,16 +0,0 @@ - - - - - com.apple.developer.ubiquity-container-identifiers - - $(TeamIdentifierPrefix)com.yodelcode.iCloudManager - - com.apple.developer.ubiquity-kvstore-identifier - $(TeamIdentifierPrefix)com.yodelcode.iCloudManager - keychain-access-groups - - $(AppIdentifierPrefix)com.yodelcode.iCloudManager - - - diff --git a/iCloudStoreManagerTests/en.lproj/InfoPlist.strings b/iCloudStoreManagerTests/en.lproj/InfoPlist.strings deleted file mode 100644 index 477b28f..0000000 --- a/iCloudStoreManagerTests/en.lproj/InfoPlist.strings +++ /dev/null @@ -1,2 +0,0 @@ -/* Localized versions of Info.plist keys */ - diff --git a/iCloudStoreManagerTests/iCloudStoreManagerTests-Info.plist b/iCloudStoreManagerTests/iCloudStoreManagerTests-Info.plist deleted file mode 100644 index 7dde709..0000000 --- a/iCloudStoreManagerTests/iCloudStoreManagerTests-Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - ${EXECUTABLE_NAME} - CFBundleIdentifier - com.yodelcode.icloudstoremanager.${PRODUCT_NAME:rfc1034identifier} - CFBundleInfoDictionaryVersion - 6.0 - CFBundlePackageType - BNDL - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1 - - diff --git a/iCloudStoreManagerTests/iCloudStoreManagerTests.h b/iCloudStoreManagerTests/iCloudStoreManagerTests.h deleted file mode 100644 index b9ea91c..0000000 --- a/iCloudStoreManagerTests/iCloudStoreManagerTests.h +++ /dev/null @@ -1,13 +0,0 @@ -// -// iCloudStoreManagerTests.h -// iCloudStoreManagerTests -// -// Created by Aleksey Novicov on 3/27/12. -// Copyright (c) 2012 Yodel Code LLC. All rights reserved. -// - -#import - -@interface iCloudStoreManagerTests : SenTestCase - -@end diff --git a/iCloudStoreManagerTests/iCloudStoreManagerTests.m b/iCloudStoreManagerTests/iCloudStoreManagerTests.m deleted file mode 100644 index bb7e5b3..0000000 --- a/iCloudStoreManagerTests/iCloudStoreManagerTests.m +++ /dev/null @@ -1,32 +0,0 @@ -// -// iCloudStoreManagerTests.m -// iCloudStoreManagerTests -// -// Created by Aleksey Novicov on 3/27/12. -// Copyright (c) 2012 Yodel Code LLC. All rights reserved. -// - -#import "iCloudStoreManagerTests.h" - -@implementation iCloudStoreManagerTests - -- (void)setUp -{ - [super setUp]; - - // Set-up code here. -} - -- (void)tearDown -{ - // Tear-down code here. - - [super tearDown]; -} - -- (void)testExample -{ - STFail(@"Unit tests are not implemented yet in iCloudStoreManagerTests"); -} - -@end diff --git a/jrswizzle b/jrswizzle new file mode 160000 index 0000000..98d18ae --- /dev/null +++ b/jrswizzle @@ -0,0 +1 @@ +Subproject commit 98d18aee73329321c320a2df85bacdb9f08a34a6