Files
CoreStore/Sources/ICloudStore.swift
2017-05-24 12:05:34 +09:00

512 lines
21 KiB
Swift

//
// ICloudStore.swift
// CoreStore
//
// Copyright © 2016 John Rommel Estropia
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
import Foundation
import CoreData
#if os(iOS) || os(OSX)
// MARK: - ICloudStore
/**
A storage interface backed by an SQLite database managed by iCloud.
*/
public final class ICloudStore: CloudStorage {
/**
Initializes an iCloud store interface from the given ubiquitous store information. Returns `nil` if the container could not be located or if iCloud storage is unavailable for the current user or device
```
guard let storage = ICloudStore(
ubiquitousContentName: "MyAppCloudData",
ubiquitousContentTransactionLogsSubdirectory: "logs/config1",
ubiquitousContainerID: "iCloud.com.mycompany.myapp.containername",
ubiquitousPeerToken: "9614d658014f4151a95d8048fb717cf0",
configuration: "Config1",
cloudStorageOptions: .recreateLocalStoreOnModelMismatch
) else {
// iCloud is not available on the device
return
}
CoreStore.addStorage(
storage,
completion: { result in
// ...
}
)
```
- parameter ubiquitousContentName: the name of the store in iCloud. This is required and should not be empty, and should not contain periods (`.`).
- parameter ubiquitousContentTransactionLogsSubdirectory: a required relative path for the transaction logs
- parameter ubiquitousContainerID: a container if your app has multiple ubiquity container identifiers in its entitlements
- parameter ubiquitousPeerToken: a per-application salt to allow multiple apps on the same device to share a Core Data store integrated with iCloud
- parameter configuration: an optional configuration name from the model file. If not specified, defaults to `nil`, the "Default" configuration. Note that if you have multiple configurations, you will need to specify a different `ubiquitousContentName` explicitly for each of them.
- parameter cloudStorageOptions: When the `ICloudStore` is passed to the `DataStack`'s `addStorage()` methods, tells the `DataStack` how to setup the persistent store. Defaults to `.None`.
*/
public required init?(ubiquitousContentName: String, ubiquitousContentTransactionLogsSubdirectory: String, ubiquitousContainerID: String? = nil, ubiquitousPeerToken: String? = nil, configuration: ModelConfiguration = nil, cloudStorageOptions: CloudStorageOptions = nil) {
CoreStore.assert(
!ubiquitousContentName.isEmpty,
"The ubiquitousContentName cannot be empty."
)
CoreStore.assert(
!ubiquitousContentName.contains("."),
"The ubiquitousContentName cannot contain periods."
)
CoreStore.assert(
!ubiquitousContentTransactionLogsSubdirectory.isEmpty,
"The ubiquitousContentURLRelativePath should not be empty."
)
CoreStore.assert(
ubiquitousPeerToken?.isEmpty != true,
"The ubiquitousPeerToken should not be empty if provided."
)
let fileManager = FileManager.default
guard let cacheFolderURL = fileManager.url(forUbiquityContainerIdentifier: ubiquitousContainerID) else {
return nil
}
let cacheFileURL = cacheFolderURL.appendingPathComponent(ubiquitousContentTransactionLogsSubdirectory)
var storeOptions: [String: Any] = [
NSSQLitePragmasOption: ["journal_mode": "WAL"],
NSPersistentStoreUbiquitousContentNameKey: ubiquitousContentName
]
storeOptions[NSPersistentStoreUbiquitousContentURLKey] = ubiquitousContentTransactionLogsSubdirectory
storeOptions[NSPersistentStoreUbiquitousContainerIdentifierKey] = ubiquitousContainerID
storeOptions[NSPersistentStoreUbiquitousPeerTokenOption] = ubiquitousPeerToken
self.cacheFileURL = cacheFileURL
self.configuration = configuration
self.cloudStorageOptions = cloudStorageOptions
self.storeOptions = storeOptions
}
/**
Registers an `ICloudStoreObserver` to start receive notifications from the ubiquitous store
- parameter observer: the observer to start sending ubiquitous notifications to
*/
public func addObserver<T: ICloudStoreObserver>(_ observer: T) {
CoreStore.assert(
Thread.isMainThread,
"Attempted to add an observer of type \(cs_typeName(observer)) outside the main thread."
)
self.removeObserver(observer)
self.registerNotification(
&self.willFinishInitialImportKey,
name: Notification.Name.iCloudUbiquitousStoreWillFinishInitialImport,
toObserver: observer,
callback: { (observer, storage, dataStack) in
observer.iCloudStoreWillFinishUbiquitousStoreInitialImport(storage: storage, dataStack: dataStack)
}
)
self.registerNotification(
&self.didFinishInitialImportKey,
name: Notification.Name.iCloudUbiquitousStoreDidFinishInitialImport,
toObserver: observer,
callback: { (observer, storage, dataStack) in
observer.iCloudStoreDidFinishUbiquitousStoreInitialImport(storage: storage, dataStack: dataStack)
}
)
self.registerNotification(
&self.willAddAccountKey,
name: Notification.Name.iCloudUbiquitousStoreWillAddAccount,
toObserver: observer,
callback: { (observer, storage, dataStack) in
observer.iCloudStoreWillAddAccount(storage: storage, dataStack: dataStack)
}
)
self.registerNotification(
&self.didAddAccountKey,
name: Notification.Name.iCloudUbiquitousStoreDidAddAccount,
toObserver: observer,
callback: { (observer, storage, dataStack) in
observer.iCloudStoreDidAddAccount(storage: storage, dataStack: dataStack)
}
)
self.registerNotification(
&self.willRemoveAccountKey,
name: Notification.Name.iCloudUbiquitousStoreWillRemoveAccount,
toObserver: observer,
callback: { (observer, storage, dataStack) in
observer.iCloudStoreWillRemoveAccount(storage: storage, dataStack: dataStack)
}
)
self.registerNotification(
&self.didRemoveAccountKey,
name: Notification.Name.iCloudUbiquitousStoreDidRemoveAccount,
toObserver: observer,
callback: { (observer, storage, dataStack) in
observer.iCloudStoreDidRemoveAccount(storage: storage, dataStack: dataStack)
}
)
self.registerNotification(
&self.willRemoveContentKey,
name: Notification.Name.iCloudUbiquitousStoreWillRemoveContent,
toObserver: observer,
callback: { (observer, storage, dataStack) in
observer.iCloudStoreWillRemoveContent(storage: storage, dataStack: dataStack)
}
)
self.registerNotification(
&self.didRemoveContentKey,
name: Notification.Name.iCloudUbiquitousStoreDidRemoveContent,
toObserver: observer,
callback: { (observer, storage, dataStack) in
observer.iCloudStoreDidRemoveContent(storage: storage, dataStack: dataStack)
}
)
}
/**
Unregisters an `ICloudStoreObserver` to stop receiving notifications from the ubiquitous store
- parameter observer: the observer to stop sending ubiquitous notifications to
*/
public func removeObserver(_ observer: ICloudStoreObserver) {
CoreStore.assert(
Thread.isMainThread,
"Attempted to remove an observer of type \(cs_typeName(observer)) outside the main thread."
)
let nilValue: AnyObject? = nil
cs_setAssociatedRetainedObject(
nilValue,
forKey: &self.willFinishInitialImportKey,
inObject: observer
)
cs_setAssociatedRetainedObject(
nilValue,
forKey: &self.didFinishInitialImportKey,
inObject: observer
)
cs_setAssociatedRetainedObject(
nilValue,
forKey: &self.willAddAccountKey,
inObject: observer
)
cs_setAssociatedRetainedObject(
nilValue,
forKey: &self.didAddAccountKey,
inObject: observer
)
cs_setAssociatedRetainedObject(
nilValue,
forKey: &self.willRemoveAccountKey,
inObject: observer
)
cs_setAssociatedRetainedObject(
nilValue,
forKey: &self.didRemoveAccountKey,
inObject: observer
)
cs_setAssociatedRetainedObject(
nilValue,
forKey: &self.willRemoveContentKey,
inObject: observer
)
cs_setAssociatedRetainedObject(
nilValue,
forKey: &self.didRemoveContentKey,
inObject: observer
)
}
// MARK: StorageInterface
/**
The string identifier for the `NSPersistentStore`'s `type` property. For `SQLiteStore`s, this is always set to `NSSQLiteStoreType`.
*/
public static let storeType = NSSQLiteStoreType
/**
The configuration name in the model file
*/
public let configuration: ModelConfiguration
/**
The options dictionary for the `NSPersistentStore`. For `SQLiteStore`s, this is always set to
```
[NSSQLitePragmasOption: ["journal_mode": "WAL"]]
```
*/
public let storeOptions: [AnyHashable: Any]?
/**
Do not call directly. Used by the `DataStack` internally.
*/
public func cs_didAddToDataStack(_ dataStack: DataStack) {
self.cs_didRemoveFromDataStack(dataStack)
self.dataStack = dataStack
let coordinator = dataStack.coordinator
cs_setAssociatedRetainedObject(
NotificationObserver(
notificationName: Notification.Name.NSPersistentStoreCoordinatorStoresWillChange,
object: coordinator,
closure: { [weak self, weak dataStack] (note) -> Void in
guard let `self` = self,
let dataStack = dataStack,
let userInfo = note.userInfo,
let transitionType = userInfo[NSPersistentStoreUbiquitousTransitionTypeKey] as? NSNumber else {
return
}
let notification: Notification.Name
switch NSPersistentStoreUbiquitousTransitionType(rawValue: transitionType.uintValue) {
case .initialImportCompleted?:
notification = Notification.Name.iCloudUbiquitousStoreWillFinishInitialImport
case .accountAdded?:
notification = Notification.Name.iCloudUbiquitousStoreWillAddAccount
case .accountRemoved?:
notification = Notification.Name.iCloudUbiquitousStoreWillRemoveAccount
case .contentRemoved?:
notification = Notification.Name.iCloudUbiquitousStoreWillRemoveContent
default:
return
}
NotificationCenter.default.post(
name: notification,
object: self,
userInfo: [String(describing: DataStack.self): dataStack]
)
}
),
forKey: &Static.persistentStoreCoordinatorWillChangeStores,
inObject: self
)
cs_setAssociatedRetainedObject(
NotificationObserver(
notificationName: NSNotification.Name.NSPersistentStoreCoordinatorStoresDidChange,
object: coordinator,
closure: { [weak self, weak dataStack] (note) -> Void in
guard let `self` = self,
let dataStack = dataStack,
let userInfo = note.userInfo,
let transitionType = userInfo[NSPersistentStoreUbiquitousTransitionTypeKey] as? NSNumber else {
return
}
let notification: Notification.Name
switch NSPersistentStoreUbiquitousTransitionType(rawValue: transitionType.uintValue) {
case .initialImportCompleted?:
notification = Notification.Name.iCloudUbiquitousStoreDidFinishInitialImport
case .accountAdded?:
notification = Notification.Name.iCloudUbiquitousStoreDidAddAccount
case .accountRemoved?:
notification = Notification.Name.iCloudUbiquitousStoreDidRemoveAccount
case .contentRemoved?:
notification = Notification.Name.iCloudUbiquitousStoreDidRemoveContent
default:
return
}
NotificationCenter.default.post(
name: notification,
object: self,
userInfo: [String(describing: DataStack.self): dataStack]
)
}
),
forKey: &Static.persistentStoreCoordinatorDidChangeStores,
inObject: self
)
}
/**
Do not call directly. Used by the `DataStack` internally.
*/
public func cs_didRemoveFromDataStack(_ dataStack: DataStack) {
let coordinator = dataStack.coordinator
let nilValue: AnyObject? = nil
cs_setAssociatedRetainedObject(
nilValue,
forKey: &Static.persistentStoreCoordinatorWillChangeStores,
inObject: coordinator
)
cs_setAssociatedRetainedObject(
nilValue,
forKey: &Static.persistentStoreCoordinatorDidChangeStores,
inObject: coordinator
)
self.dataStack = nil
}
// MARK: CloudStorage
/**
The `NSURL` that points to the ubiquity container file
*/
public let cacheFileURL: URL
/**
Options that tell the `DataStack` how to setup the persistent store
*/
public var cloudStorageOptions: CloudStorageOptions
/**
The options dictionary for the specified `CloudStorageOptions`
*/
public func dictionary(forOptions options: CloudStorageOptions) -> [AnyHashable: Any]? {
if options == .none {
return self.storeOptions
}
var storeOptions = self.storeOptions ?? [:]
if options.contains(.allowSynchronousLightweightMigration) {
storeOptions[NSMigratePersistentStoresAutomaticallyOption] = true
storeOptions[NSInferMappingModelAutomaticallyOption] = true
}
if options.contains(.recreateLocalStoreOnModelMismatch) {
storeOptions[NSPersistentStoreRebuildFromUbiquitousContentOption] = true
}
return storeOptions
}
/**
Called by the `DataStack` to perform actual deletion of the store file from disk. Do not call directly! The `sourceModel` argument is a hint for the existing store's model version. For `SQLiteStore`, this converts the database's WAL journaling mode to DELETE before deleting the file.
*/
public func cs_eraseStorageAndWait(soureModel: NSManagedObjectModel) throws {
// TODO: check if attached to persistent store
let cacheFileURL = self.cacheFileURL
try autoreleasepool {
let journalUpdatingCoordinator = NSPersistentStoreCoordinator(managedObjectModel: soureModel)
let options = [
NSSQLitePragmasOption: ["journal_mode": "DELETE"],
NSPersistentStoreRemoveUbiquitousMetadataOption: true
] as [String : Any]
let store = try journalUpdatingCoordinator.addPersistentStore(
ofType: type(of: self).storeType,
configurationName: self.configuration,
at: cacheFileURL,
options: options
)
try journalUpdatingCoordinator.remove(store)
try NSPersistentStoreCoordinator.removeUbiquitousContentAndPersistentStore(
at: cacheFileURL,
options: options
)
}
}
// MARK: Private
fileprivate struct Static {
fileprivate static var persistentStoreCoordinatorWillChangeStores: Void?
fileprivate static var persistentStoreCoordinatorDidChangeStores: Void?
}
private var willFinishInitialImportKey: Void?
private var didFinishInitialImportKey: Void?
private var willAddAccountKey: Void?
private var didAddAccountKey: Void?
private var willRemoveAccountKey: Void?
private var didRemoveAccountKey: Void?
private var willRemoveContentKey: Void?
private var didRemoveContentKey: Void?
private weak var dataStack: DataStack?
private func registerNotification<T: ICloudStoreObserver>(_ notificationKey: UnsafeRawPointer, name: Notification.Name, toObserver observer: T, callback: @escaping (_ observer: T, _ storage: ICloudStore, _ dataStack: DataStack) -> Void) {
cs_setAssociatedRetainedObject(
NotificationObserver(
notificationName: name,
object: self,
closure: { [weak self, weak observer] (note) -> Void in
guard let `self` = self,
let observer = observer,
let dataStack = note.userInfo?[String(describing: DataStack.self)] as? DataStack,
self.dataStack === dataStack else {
return
}
callback(observer, self, dataStack)
}
),
forKey: notificationKey,
inObject: observer
)
}
}
// MARK: - Notification Keys
fileprivate extension Notification.Name {
fileprivate static let iCloudUbiquitousStoreWillFinishInitialImport = Notification.Name(rawValue: "iCloudUbiquitousStoreWillFinishInitialImport")
fileprivate static let iCloudUbiquitousStoreDidFinishInitialImport = Notification.Name(rawValue: "iCloudUbiquitousStoreDidFinishInitialImport")
fileprivate static let iCloudUbiquitousStoreWillAddAccount = Notification.Name(rawValue: "iCloudUbiquitousStoreWillAddAccount")
fileprivate static let iCloudUbiquitousStoreDidAddAccount = Notification.Name(rawValue: "iCloudUbiquitousStoreDidAddAccount")
fileprivate static let iCloudUbiquitousStoreWillRemoveAccount = Notification.Name(rawValue: "iCloudUbiquitousStoreWillRemoveAccount")
fileprivate static let iCloudUbiquitousStoreDidRemoveAccount = Notification.Name(rawValue: "iCloudUbiquitousStoreDidRemoveAccount")
fileprivate static let iCloudUbiquitousStoreWillRemoveContent = Notification.Name(rawValue: "iCloudUbiquitousStoreWillRemoveContent")
fileprivate static let iCloudUbiquitousStoreDidRemoveContent = Notification.Name(rawValue: "iCloudUbiquitousStoreDidRemoveContent")
}
#endif