mirror of
https://github.com/JohnEstropia/CoreStore.git
synced 2026-04-01 15:13:06 +02:00
added NSProgress support for migrations
This commit is contained in:
@@ -62,6 +62,8 @@
|
|||||||
B5E84F381AFF85470064E85B /* NSManagedObject+Transaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E84F341AFF85470064E85B /* NSManagedObject+Transaction.swift */; };
|
B5E84F381AFF85470064E85B /* NSManagedObject+Transaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E84F341AFF85470064E85B /* NSManagedObject+Transaction.swift */; };
|
||||||
B5E84F391AFF85470064E85B /* NSManagedObjectContext+Querying.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E84F351AFF85470064E85B /* NSManagedObjectContext+Querying.swift */; };
|
B5E84F391AFF85470064E85B /* NSManagedObjectContext+Querying.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E84F351AFF85470064E85B /* NSManagedObjectContext+Querying.swift */; };
|
||||||
B5E84F411AFF8CCD0064E85B /* ClauseTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E84F401AFF8CCD0064E85B /* ClauseTypes.swift */; };
|
B5E84F411AFF8CCD0064E85B /* ClauseTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E84F401AFF8CCD0064E85B /* ClauseTypes.swift */; };
|
||||||
|
B5FAD6A91B50A4B400714891 /* NSProgress+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5FAD6A81B50A4B300714891 /* NSProgress+Convenience.swift */; };
|
||||||
|
B5FAD6AC1B51285300714891 /* MigrationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5FAD6AB1B51285300714891 /* MigrationManager.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
@@ -158,6 +160,8 @@
|
|||||||
B5E84F341AFF85470064E85B /* NSManagedObject+Transaction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSManagedObject+Transaction.swift"; sourceTree = "<group>"; };
|
B5E84F341AFF85470064E85B /* NSManagedObject+Transaction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSManagedObject+Transaction.swift"; sourceTree = "<group>"; };
|
||||||
B5E84F351AFF85470064E85B /* NSManagedObjectContext+Querying.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext+Querying.swift"; sourceTree = "<group>"; };
|
B5E84F351AFF85470064E85B /* NSManagedObjectContext+Querying.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext+Querying.swift"; sourceTree = "<group>"; };
|
||||||
B5E84F401AFF8CCD0064E85B /* ClauseTypes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClauseTypes.swift; sourceTree = "<group>"; };
|
B5E84F401AFF8CCD0064E85B /* ClauseTypes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClauseTypes.swift; sourceTree = "<group>"; };
|
||||||
|
B5FAD6A81B50A4B300714891 /* NSProgress+Convenience.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSProgress+Convenience.swift"; sourceTree = "<group>"; };
|
||||||
|
B5FAD6AB1B51285300714891 /* MigrationManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MigrationManager.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@@ -383,6 +387,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
B5E84F271AFF84920064E85B /* NSManagedObject+Convenience.swift */,
|
B5E84F271AFF84920064E85B /* NSManagedObject+Convenience.swift */,
|
||||||
|
B5FAD6A81B50A4B300714891 /* NSProgress+Convenience.swift */,
|
||||||
);
|
);
|
||||||
path = "Convenience Helpers";
|
path = "Convenience Helpers";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -391,13 +396,14 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
B5E84F2A1AFF849C0064E85B /* AssociatedObjects.swift */,
|
B5E84F2A1AFF849C0064E85B /* AssociatedObjects.swift */,
|
||||||
B51BE0691B47FC4B0069F532 /* NSManagedObjectModel+Setup.swift */,
|
|
||||||
B5E84F2B1AFF849C0064E85B /* NotificationObserver.swift */,
|
B5E84F2B1AFF849C0064E85B /* NotificationObserver.swift */,
|
||||||
|
B5FAD6AB1B51285300714891 /* MigrationManager.swift */,
|
||||||
B5E84F341AFF85470064E85B /* NSManagedObject+Transaction.swift */,
|
B5E84F341AFF85470064E85B /* NSManagedObject+Transaction.swift */,
|
||||||
B5E84F2C1AFF849C0064E85B /* NSManagedObjectContext+CoreStore.swift */,
|
B5E84F2C1AFF849C0064E85B /* NSManagedObjectContext+CoreStore.swift */,
|
||||||
B5E84F351AFF85470064E85B /* NSManagedObjectContext+Querying.swift */,
|
B5E84F351AFF85470064E85B /* NSManagedObjectContext+Querying.swift */,
|
||||||
B5E84F321AFF85470064E85B /* NSManagedObjectContext+Setup.swift */,
|
B5E84F321AFF85470064E85B /* NSManagedObjectContext+Setup.swift */,
|
||||||
B5E84F331AFF85470064E85B /* NSManagedObjectContext+Transaction.swift */,
|
B5E84F331AFF85470064E85B /* NSManagedObjectContext+Transaction.swift */,
|
||||||
|
B51BE0691B47FC4B0069F532 /* NSManagedObjectModel+Setup.swift */,
|
||||||
B5E84F2D1AFF849C0064E85B /* WeakObject.swift */,
|
B5E84F2D1AFF849C0064E85B /* WeakObject.swift */,
|
||||||
);
|
);
|
||||||
path = Internal;
|
path = Internal;
|
||||||
@@ -558,6 +564,7 @@
|
|||||||
2F291E2719C6D3CF007AF63F /* CoreStore.swift in Sources */,
|
2F291E2719C6D3CF007AF63F /* CoreStore.swift in Sources */,
|
||||||
B5E84F411AFF8CCD0064E85B /* ClauseTypes.swift in Sources */,
|
B5E84F411AFF8CCD0064E85B /* ClauseTypes.swift in Sources */,
|
||||||
B5E84F0D1AFF847B0064E85B /* BaseDataTransaction+Querying.swift in Sources */,
|
B5E84F0D1AFF847B0064E85B /* BaseDataTransaction+Querying.swift in Sources */,
|
||||||
|
B5FAD6AC1B51285300714891 /* MigrationManager.swift in Sources */,
|
||||||
B5E84EF61AFF846E0064E85B /* DataStack+Transaction.swift in Sources */,
|
B5E84EF61AFF846E0064E85B /* DataStack+Transaction.swift in Sources */,
|
||||||
B5E84EDF1AFF84500064E85B /* DataStack.swift in Sources */,
|
B5E84EDF1AFF84500064E85B /* DataStack.swift in Sources */,
|
||||||
B5E84F231AFF84860064E85B /* ListMonitor.swift in Sources */,
|
B5E84F231AFF84860064E85B /* ListMonitor.swift in Sources */,
|
||||||
@@ -566,6 +573,7 @@
|
|||||||
B5E84EF51AFF846E0064E85B /* BaseDataTransaction.swift in Sources */,
|
B5E84EF51AFF846E0064E85B /* BaseDataTransaction.swift in Sources */,
|
||||||
B5E84EFB1AFF846E0064E85B /* SaveResult.swift in Sources */,
|
B5E84EFB1AFF846E0064E85B /* SaveResult.swift in Sources */,
|
||||||
B5E84F0F1AFF847B0064E85B /* From.swift in Sources */,
|
B5E84F0F1AFF847B0064E85B /* From.swift in Sources */,
|
||||||
|
B5FAD6A91B50A4B400714891 /* NSProgress+Convenience.swift in Sources */,
|
||||||
B5E84EFC1AFF846E0064E85B /* SynchronousDataTransaction.swift in Sources */,
|
B5E84EFC1AFF846E0064E85B /* SynchronousDataTransaction.swift in Sources */,
|
||||||
B5E84F281AFF84920064E85B /* NSManagedObject+Convenience.swift in Sources */,
|
B5E84F281AFF84920064E85B /* NSManagedObject+Convenience.swift in Sources */,
|
||||||
B51BE06A1B47FC4B0069F532 /* NSManagedObjectModel+Setup.swift in Sources */,
|
B51BE06A1B47FC4B0069F532 /* NSManagedObjectModel+Setup.swift in Sources */,
|
||||||
|
|||||||
112
CoreStore/Convenience Helpers/NSProgress+Convenience.swift
Normal file
112
CoreStore/Convenience Helpers/NSProgress+Convenience.swift
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
//
|
||||||
|
// NSProgress+Convenience.swift
|
||||||
|
// CoreStore
|
||||||
|
//
|
||||||
|
// Copyright (c) 2015 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 GCDKit
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - NSProgress
|
||||||
|
|
||||||
|
public extension NSProgress {
|
||||||
|
|
||||||
|
// MARK: Public
|
||||||
|
|
||||||
|
public func setProgressHandler(closure: ((progress: NSProgress) -> Void)?) {
|
||||||
|
|
||||||
|
self.progressObserver.progressHandler = closure
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: Private
|
||||||
|
|
||||||
|
private struct PropertyKeys {
|
||||||
|
|
||||||
|
static var progressObserver: Void?
|
||||||
|
}
|
||||||
|
|
||||||
|
private var progressObserver: ProgressObserver {
|
||||||
|
|
||||||
|
get {
|
||||||
|
|
||||||
|
let object: AnyObject? = getAssociatedObjectForKey(&PropertyKeys.progressObserver, inObject: self)
|
||||||
|
|
||||||
|
if let observer = object as? ProgressObserver {
|
||||||
|
|
||||||
|
return observer
|
||||||
|
}
|
||||||
|
|
||||||
|
let observer = ProgressObserver(self)
|
||||||
|
setAssociatedRetainedObject(
|
||||||
|
observer,
|
||||||
|
forKey: &PropertyKeys.progressObserver,
|
||||||
|
inObject: self
|
||||||
|
)
|
||||||
|
|
||||||
|
return observer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@objc private final class ProgressObserver: NSObject {
|
||||||
|
|
||||||
|
private weak var progress: NSProgress?
|
||||||
|
private var progressHandler: ((progress: NSProgress) -> Void)?
|
||||||
|
|
||||||
|
private init(_ progress: NSProgress) {
|
||||||
|
|
||||||
|
self.progress = progress
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
progress.addObserver(
|
||||||
|
self,
|
||||||
|
forKeyPath: "fractionCompleted",
|
||||||
|
options: .New,
|
||||||
|
context: nil
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
|
||||||
|
progress?.removeObserver(self, forKeyPath: "fractionCompleted")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
|
||||||
|
|
||||||
|
guard let progress = self.progress where object as? NSProgress == progress && keyPath == "fractionCompleted" else {
|
||||||
|
|
||||||
|
super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
GCDQueue.Main.async { [weak self] () -> Void in
|
||||||
|
|
||||||
|
if let strongSelf = self, let progress = strongSelf.progress {
|
||||||
|
|
||||||
|
strongSelf.progressHandler?(progress: progress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
61
CoreStore/Internal/MigrationManager.swift
Normal file
61
CoreStore/Internal/MigrationManager.swift
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
//
|
||||||
|
// MigrationManager.swift
|
||||||
|
// CoreStore
|
||||||
|
//
|
||||||
|
// Copyright (c) 2015 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
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - MigrationManager
|
||||||
|
|
||||||
|
internal final class MigrationManager: NSMigrationManager, NSProgressReporting {
|
||||||
|
|
||||||
|
// MARK: NSObject
|
||||||
|
|
||||||
|
override func didChangeValueForKey(key: String) {
|
||||||
|
|
||||||
|
super.didChangeValueForKey(key)
|
||||||
|
|
||||||
|
if key == "migrationProgress" {
|
||||||
|
|
||||||
|
let progress = self.progress
|
||||||
|
progress.completedUnitCount = Int64(Float(progress.totalUnitCount) * self.migrationProgress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: NSMigrationManager
|
||||||
|
|
||||||
|
init(sourceModel: NSManagedObjectModel, destinationModel: NSManagedObjectModel, progress: NSProgress) {
|
||||||
|
|
||||||
|
self.progress = progress
|
||||||
|
|
||||||
|
super.init(sourceModel: sourceModel, destinationModel: destinationModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: NSProgressReporting
|
||||||
|
|
||||||
|
let progress: NSProgress
|
||||||
|
}
|
||||||
@@ -74,7 +74,8 @@ internal extension NSManagedObjectContext {
|
|||||||
|
|
||||||
let context = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType)
|
let context = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType)
|
||||||
context.parentContext = rootContext
|
context.parentContext = rootContext
|
||||||
context.shouldCascadeSavesToParent = true
|
context.mergePolicy = NSRollbackMergePolicy
|
||||||
|
context.shouldCascadeSavesToParent = false
|
||||||
context.undoManager = nil
|
context.undoManager = nil
|
||||||
context.setupForCoreStoreWithContextName("com.corestore.maincontext")
|
context.setupForCoreStoreWithContextName("com.corestore.maincontext")
|
||||||
context.observerForDidSaveNotification = NotificationObserver(
|
context.observerForDidSaveNotification = NotificationObserver(
|
||||||
|
|||||||
@@ -33,15 +33,65 @@ internal extension NSManagedObjectModel {
|
|||||||
|
|
||||||
// MARK: Internal
|
// MARK: Internal
|
||||||
|
|
||||||
private var modelFileURL: NSURL? {
|
@nonobjc internal class func fromBundle(bundle: NSBundle, modelName: String, modelVersion: String? = nil) -> NSManagedObjectModel {
|
||||||
|
|
||||||
get {
|
guard let modelFilePath = bundle.pathForResource(modelName, ofType: "momd") else {
|
||||||
|
|
||||||
return self.modelVersionFileURL?.URLByDeletingLastPathComponent
|
fatalError("Could not find \"\(modelName).momd\" from the bundle. \(bundle)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let modelFileURL = NSURL(fileURLWithPath: modelFilePath)
|
||||||
|
let versionInfoPlistURL = modelFileURL.URLByAppendingPathComponent("VersionInfo.plist", isDirectory: false)
|
||||||
|
|
||||||
|
guard let versionInfo = NSDictionary(contentsOfURL: versionInfoPlistURL),
|
||||||
|
let versionHashes = versionInfo["NSManagedObjectModel_VersionHashes"] as? [String: AnyObject] else {
|
||||||
|
|
||||||
|
fatalError("Could not load \(typeName(NSManagedObjectModel)) metadata from path \"\(versionInfoPlistURL)\"."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let modelVersions = Set(versionHashes.keys)
|
||||||
|
let currentModelVersion: String
|
||||||
|
|
||||||
|
if let modelVersion = modelVersion {
|
||||||
|
|
||||||
|
currentModelVersion = modelVersion
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
|
||||||
|
currentModelVersion = versionInfo["NSManagedObjectModel_CurrentVersionName"] as? String ?? modelVersions.first!
|
||||||
|
}
|
||||||
|
|
||||||
|
var modelVersionFileURL: NSURL?
|
||||||
|
for modelVersion in modelVersions {
|
||||||
|
|
||||||
|
let fileURL = modelFileURL.URLByAppendingPathComponent("\(modelVersion).mom", isDirectory: false)
|
||||||
|
|
||||||
|
if modelVersion == currentModelVersion {
|
||||||
|
|
||||||
|
modelVersionFileURL = fileURL
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
precondition(
|
||||||
|
NSManagedObjectModel(contentsOfURL: fileURL) != nil,
|
||||||
|
"Could not find the \"\(modelVersion).mom\" version file for the model at URL \"\(modelFileURL)\"."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let modelVersionFileURL = modelVersionFileURL,
|
||||||
|
let rootModel = NSManagedObjectModel(contentsOfURL: modelVersionFileURL) {
|
||||||
|
|
||||||
|
rootModel.modelVersionFileURL = modelVersionFileURL
|
||||||
|
rootModel.modelVersions = modelVersions
|
||||||
|
rootModel.currentModelVersion = currentModelVersion
|
||||||
|
return rootModel
|
||||||
|
}
|
||||||
|
|
||||||
|
fatalError("Could not create an \(typeName(NSManagedObjectModel)) from the model at URL \"\(modelFileURL)\".")
|
||||||
}
|
}
|
||||||
|
|
||||||
private(set) var currentModelVersion: String? {
|
@nonobjc private(set) internal var currentModelVersion: String? {
|
||||||
|
|
||||||
get {
|
get {
|
||||||
|
|
||||||
@@ -61,7 +111,7 @@ internal extension NSManagedObjectModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private(set) var modelVersions: Set<String>? {
|
@nonobjc private(set) internal var modelVersions: Set<String>? {
|
||||||
|
|
||||||
get {
|
get {
|
||||||
|
|
||||||
@@ -81,17 +131,17 @@ internal extension NSManagedObjectModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func entityNameForClass(entityClass: AnyClass) -> String {
|
@nonobjc internal func entityNameForClass(entityClass: AnyClass) -> String {
|
||||||
|
|
||||||
return self.entityNameMapping[NSStringFromClass(entityClass)]!
|
return self.entityNameMapping[NSStringFromClass(entityClass)]!
|
||||||
}
|
}
|
||||||
|
|
||||||
func mergedModels() -> [NSManagedObjectModel] {
|
@nonobjc internal func mergedModels() -> [NSManagedObjectModel] {
|
||||||
|
|
||||||
return self.modelVersions?.map { self[$0] }.flatMap { $0 == nil ? [] : [$0!] } ?? [self]
|
return self.modelVersions?.map { self[$0] }.flatMap { $0 == nil ? [] : [$0!] } ?? [self]
|
||||||
}
|
}
|
||||||
|
|
||||||
subscript(modelVersion: String) -> NSManagedObjectModel? {
|
@nonobjc internal subscript(modelVersion: String) -> NSManagedObjectModel? {
|
||||||
|
|
||||||
if modelVersion == self.currentModelVersion {
|
if modelVersion == self.currentModelVersion {
|
||||||
|
|
||||||
@@ -117,68 +167,32 @@ internal extension NSManagedObjectModel {
|
|||||||
return model
|
return model
|
||||||
}
|
}
|
||||||
|
|
||||||
class func fromBundle(bundle: NSBundle, modelName: String, modelVersion: String? = nil) -> NSManagedObjectModel {
|
@nonobjc internal subscript(metadata: [String: AnyObject]) -> NSManagedObjectModel? {
|
||||||
|
|
||||||
guard let modelFilePath = bundle.pathForResource(modelName, ofType: "momd") else {
|
if let modelHashes = metadata[NSStoreModelVersionHashesKey] as? [String : NSData] {
|
||||||
|
|
||||||
CoreStore.fatalError("Could not find \"\(modelName).momd\" from the bundle. \(bundle)")
|
for modelVersion in self.modelVersions ?? [] {
|
||||||
}
|
|
||||||
|
|
||||||
let modelFileURL = NSURL(fileURLWithPath: modelFilePath)
|
|
||||||
let versionInfoPlistURL = modelFileURL.URLByAppendingPathComponent("VersionInfo.plist", isDirectory: false)
|
|
||||||
|
|
||||||
guard let versionInfo = NSDictionary(contentsOfURL: versionInfoPlistURL),
|
|
||||||
let versionHashes = versionInfo["NSManagedObjectModel_VersionHashes"] as? [String: AnyObject] else {
|
|
||||||
|
|
||||||
CoreStore.fatalError("Could not load \(typeName(NSManagedObjectModel)) metadata from path \"\(versionInfoPlistURL)\"."
|
if let versionModel = self[modelVersion] where modelHashes == versionModel.entityVersionHashesByName {
|
||||||
)
|
|
||||||
}
|
return versionModel
|
||||||
|
}
|
||||||
let modelVersions = Set(versionHashes.keys)
|
|
||||||
let currentModelVersion: String
|
|
||||||
|
|
||||||
if let modelVersion = modelVersion {
|
|
||||||
|
|
||||||
precondition(modelVersions.contains(modelVersion))
|
|
||||||
currentModelVersion = modelVersion
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
|
|
||||||
currentModelVersion = versionInfo["NSManagedObjectModel_CurrentVersionName"] as? String ?? modelVersions.first!
|
|
||||||
}
|
|
||||||
|
|
||||||
var modelVersionFileURL: NSURL?
|
|
||||||
for modelVersion in modelVersions {
|
|
||||||
|
|
||||||
let fileURL = modelFileURL.URLByAppendingPathComponent("\(modelVersion).mom", isDirectory: false)
|
|
||||||
|
|
||||||
if modelVersion == currentModelVersion {
|
|
||||||
|
|
||||||
modelVersionFileURL = fileURL
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CoreStore.assert(
|
|
||||||
NSManagedObjectModel(contentsOfURL: fileURL) != nil,
|
|
||||||
"Could not find the \"\(modelVersion).mom\" version file for the model at URL \"\(modelFileURL)\"."
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
if let modelVersionFileURL = modelVersionFileURL,
|
|
||||||
let rootModel = NSManagedObjectModel(contentsOfURL: modelVersionFileURL) {
|
|
||||||
|
|
||||||
rootModel.modelVersionFileURL = modelVersionFileURL
|
|
||||||
rootModel.modelVersions = modelVersions
|
|
||||||
rootModel.currentModelVersion = currentModelVersion
|
|
||||||
return rootModel
|
|
||||||
}
|
|
||||||
|
|
||||||
CoreStore.fatalError("Could not create an \(typeName(NSManagedObjectModel)) from the model at URL \"\(modelFileURL)\".")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// MARK: Private
|
// MARK: Private
|
||||||
|
|
||||||
|
private var modelFileURL: NSURL? {
|
||||||
|
|
||||||
|
get {
|
||||||
|
|
||||||
|
return self.modelVersionFileURL?.URLByDeletingLastPathComponent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var modelVersionFileURL: NSURL? {
|
private var modelVersionFileURL: NSURL? {
|
||||||
|
|
||||||
get {
|
get {
|
||||||
|
|||||||
@@ -46,10 +46,8 @@ public extension CoreStore {
|
|||||||
level: level,
|
level: level,
|
||||||
message: message,
|
message: message,
|
||||||
fileName: fileName,
|
fileName: fileName,
|
||||||
lineNumber:
|
lineNumber: lineNumber,
|
||||||
lineNumber,
|
functionName: functionName
|
||||||
functionName:
|
|
||||||
functionName
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,14 +72,4 @@ public extension CoreStore {
|
|||||||
functionName: functionName
|
functionName: functionName
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@noreturn internal static func fatalError(message: String, fileName: StaticString = __FILE__, lineNumber: Int = __LINE__, functionName: StaticString = __FUNCTION__) {
|
|
||||||
|
|
||||||
self.logger.fatalError(
|
|
||||||
message,
|
|
||||||
fileName: fileName,
|
|
||||||
lineNumber: lineNumber,
|
|
||||||
functionName: functionName
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,16 +79,6 @@ public protocol CoreStoreLogger {
|
|||||||
:functionName: the source function name
|
:functionName: the source function name
|
||||||
*/
|
*/
|
||||||
func assert(@autoclosure condition: () -> Bool, message: String, fileName: StaticString, lineNumber: Int, functionName: StaticString)
|
func assert(@autoclosure condition: () -> Bool, message: String, fileName: StaticString, lineNumber: Int, functionName: StaticString)
|
||||||
|
|
||||||
/**
|
|
||||||
Handles fatal errors made throughout the `CoreStore` framework. Implementations should guarantee that the method does not return, either by calling fatalError() or preconditionFailure(), or by raising an exception.
|
|
||||||
|
|
||||||
:message: the error message
|
|
||||||
:fileName: the source file name
|
|
||||||
:lineNumber: the source line number
|
|
||||||
:functionName: the source function name
|
|
||||||
*/
|
|
||||||
@noreturn func fatalError(message: String, fileName: StaticString, lineNumber: Int, functionName: StaticString)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -96,17 +86,17 @@ public protocol CoreStoreLogger {
|
|||||||
|
|
||||||
internal func typeName<T>(value: T) -> String {
|
internal func typeName<T>(value: T) -> String {
|
||||||
|
|
||||||
return "<\(_stdlib_getDemangledTypeName(value))>"
|
return "'\(_stdlib_getDemangledTypeName(value))'"
|
||||||
}
|
}
|
||||||
|
|
||||||
internal func typeName<T>(value: T.Type) -> String {
|
internal func typeName<T>(value: T.Type) -> String {
|
||||||
|
|
||||||
return "<\(value)>"
|
return "'\(value)'"
|
||||||
}
|
}
|
||||||
|
|
||||||
internal func typeName(value: AnyClass) -> String {
|
internal func typeName(value: AnyClass) -> String {
|
||||||
|
|
||||||
return "<\(value)>"
|
return "'\(value)'"
|
||||||
}
|
}
|
||||||
|
|
||||||
internal func typeName(name: String?) -> String {
|
internal func typeName(name: String?) -> String {
|
||||||
|
|||||||
@@ -42,33 +42,41 @@ public final class DefaultLogger: CoreStoreLogger {
|
|||||||
public func log(level level: LogLevel, message: String, fileName: StaticString, lineNumber: Int, functionName: StaticString) {
|
public func log(level level: LogLevel, message: String, fileName: StaticString, lineNumber: Int, functionName: StaticString) {
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
|
let icon: String
|
||||||
let levelString: String
|
let levelString: String
|
||||||
switch level {
|
switch level {
|
||||||
case .Trace: levelString = "Trace"
|
|
||||||
case .Notice: levelString = "Notice"
|
case .Trace:
|
||||||
case .Warning: levelString = "Warning"
|
icon = "🔹"
|
||||||
case .Fatal: levelString = "Fatal"
|
levelString = "Trace"
|
||||||
|
|
||||||
|
case .Notice:
|
||||||
|
icon = "🔸"
|
||||||
|
levelString = "Notice"
|
||||||
|
|
||||||
|
case .Warning:
|
||||||
|
icon = "⚠️"
|
||||||
|
levelString = "Warning"
|
||||||
|
|
||||||
|
case .Fatal:
|
||||||
|
icon = "❗"
|
||||||
|
levelString = "Fatal"
|
||||||
}
|
}
|
||||||
Swift.print("[CoreStore:\(levelString)] \(fileName.stringValue.lastPathComponent):\(lineNumber) \(functionName)\n ↪︎ \(message)\n")
|
Swift.print("\(icon) [CoreStore: \(levelString)] \(fileName.stringValue.lastPathComponent):\(lineNumber) \(functionName)\n ↪︎ \(message)\n")
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
public func handleError(error error: NSError, message: String, fileName: StaticString, lineNumber: Int, functionName: StaticString) {
|
public func handleError(error error: NSError, message: String, fileName: StaticString, lineNumber: Int, functionName: StaticString) {
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
Swift.print("[CoreStore:Error] \(fileName.stringValue.lastPathComponent):\(lineNumber) \(functionName)\n ↪︎ \(message): \(error)\n")
|
Swift.print("⚠️ [CoreStore: Error] \(fileName.stringValue.lastPathComponent):\(lineNumber) \(functionName)\n ↪︎ \(message): \(error)\n")
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
public func assert(@autoclosure condition: () -> Bool, message: String, fileName: StaticString, lineNumber: Int, functionName: StaticString) {
|
public func assert(@autoclosure condition: () -> Bool, message: String, fileName: StaticString, lineNumber: Int, functionName: StaticString) {
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
Swift.assert(condition, message, file: fileName, line: numericCast(lineNumber))
|
Swift.assert(condition, "❗ [CoreStore: Assertion Failure] \(message)", file: fileName, line: numericCast(lineNumber))
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@noreturn public func fatalError(message: String, fileName: StaticString, lineNumber: Int, functionName: StaticString) {
|
|
||||||
|
|
||||||
Swift.fatalError("[CoreStore:Abort] \(fileName.stringValue.lastPathComponent):\(lineNumber) \(functionName)\n ↪︎ \(message)\n")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,16 +33,201 @@ import GCDKit
|
|||||||
public extension DataStack {
|
public extension DataStack {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Checks if the store with the specified filename and configuration needs to be migrated to the `DataStack`'s managed object model version.
|
Asynchronously adds to the stack an SQLite store from the given SQLite file name. Note that using `addSQLiteStore(...)` instead of `addSQLiteStoreAndWait(...)` implies that the migrations are allowed and expected (thus the asynchronous `completion`.)
|
||||||
|
|
||||||
|
- parameter fileName: the local filename for the SQLite persistent store in the "Application Support" directory. A new SQLite file will be created if it does not exist. Note that if you have multiple configurations, you will need to specify a different `fileName` explicitly for each of them.
|
||||||
|
- 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 `fileName` explicitly for each of them.
|
||||||
|
- parameter mappingModelBundles: an optional array of bundles to search mapping model files from. If not set, defaults to the `NSBundle.allBundles()`.
|
||||||
|
- parameter completion: the closure to be executed on the main queue when the process completes, either due to success or failure. The closure's `PersistentStoreResult` argument indicates the result. If an error is thrown, this closure will not be executed.
|
||||||
|
- returns: an `NSProgress` instance if a migration has started, or `nil` is no migrations are required
|
||||||
|
*/
|
||||||
|
public func addSQLiteStore(fileName fileName: String, configuration: String? = nil, mappingModelBundles: [NSBundle]? = nil, completion: (PersistentStoreResult) -> Void) throws -> NSProgress? {
|
||||||
|
|
||||||
|
return try self.addSQLiteStore(
|
||||||
|
fileURL: applicationSupportDirectory.URLByAppendingPathComponent(
|
||||||
|
fileName,
|
||||||
|
isDirectory: false
|
||||||
|
),
|
||||||
|
configuration: configuration,
|
||||||
|
mappingModelBundles: mappingModelBundles,
|
||||||
|
completion: completion
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Asynchronously adds to the stack an SQLite store from the given SQLite file URL. Note that using `addSQLiteStore(...)` instead of `addSQLiteStoreAndWait(...)` implies that the migrations are allowed and expected (thus the asynchronous `completion`.)
|
||||||
|
|
||||||
|
- parameter fileURL: the local file URL for the SQLite persistent store. A new SQLite file will be created if it does not exist. If not specified, defaults to a file URL pointing to a "<Application name>.sqlite" file in the "Application Support" directory. Note that if you have multiple configurations, you will need to specify a different `fileURL` explicitly for each of them.
|
||||||
|
- 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 `fileURL` explicitly for each of them.
|
||||||
|
- parameter mappingModelBundles: an optional array of bundles to search mapping model files from. If not set, defaults to the `NSBundle.allBundles()`.
|
||||||
|
- parameter completion: the closure to be executed on the main queue when the process completes, either due to success or failure. The closure's `PersistentStoreResult` argument indicates the result.
|
||||||
|
- returns: an `NSProgress` instance if a migration has started, or `nil` is no migrations are required
|
||||||
|
*/
|
||||||
|
public func addSQLiteStore(fileURL fileURL: NSURL = defaultSQLiteStoreURL, configuration: String? = nil, mappingModelBundles: [NSBundle]? = NSBundle.allBundles(), completion: (PersistentStoreResult) -> Void) throws -> NSProgress? {
|
||||||
|
|
||||||
|
CoreStore.assert(
|
||||||
|
fileURL.fileURL,
|
||||||
|
"The specified file URL for the SQLite store is invalid: \"\(fileURL)\""
|
||||||
|
)
|
||||||
|
|
||||||
|
let coordinator = self.coordinator;
|
||||||
|
if let store = coordinator.persistentStoreForURL(fileURL) {
|
||||||
|
|
||||||
|
let isExistingStoreAutomigrating = store.options?[NSMigratePersistentStoresAutomaticallyOption] as? Bool == true
|
||||||
|
|
||||||
|
if store.type == NSSQLiteStoreType
|
||||||
|
&& isExistingStoreAutomigrating
|
||||||
|
&& store.configurationName == (configuration ?? Into.defaultConfigurationName) {
|
||||||
|
|
||||||
|
GCDQueue.Main.async {
|
||||||
|
|
||||||
|
completion(PersistentStoreResult(store))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let error = NSError(coreStoreErrorCode: .DifferentPersistentStoreExistsAtURL)
|
||||||
|
CoreStore.handleError(
|
||||||
|
error,
|
||||||
|
"Failed to add SQLite \(typeName(NSPersistentStore)) at \"\(fileURL)\" because a different \(typeName(NSPersistentStore)) at that URL already exists."
|
||||||
|
)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
|
||||||
|
try NSFileManager.defaultManager().createDirectoryAtURL(
|
||||||
|
fileURL.URLByDeletingLastPathComponent!,
|
||||||
|
withIntermediateDirectories: true,
|
||||||
|
attributes: nil
|
||||||
|
)
|
||||||
|
}
|
||||||
|
catch _ { }
|
||||||
|
|
||||||
|
do {
|
||||||
|
|
||||||
|
let metadata = try NSPersistentStoreCoordinator.metadataForPersistentStoreOfType(
|
||||||
|
NSSQLiteStoreType,
|
||||||
|
URL: fileURL
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.upgradeSQLiteStoreIfNeeded(
|
||||||
|
fileURL: fileURL,
|
||||||
|
metadata: metadata,
|
||||||
|
configuration: configuration,
|
||||||
|
mappingModelBundles: mappingModelBundles,
|
||||||
|
completion: { (result) -> Void in
|
||||||
|
|
||||||
|
if case .Failure(let error) = result {
|
||||||
|
|
||||||
|
completion(PersistentStoreResult(error))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let persistentStoreResult = self.addSQLiteStoreAndWait(
|
||||||
|
fileURL: fileURL,
|
||||||
|
configuration: configuration,
|
||||||
|
automigrating: false,
|
||||||
|
resetStoreOnMigrationFailure: false
|
||||||
|
)
|
||||||
|
|
||||||
|
completion(persistentStoreResult)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
catch let error as NSError
|
||||||
|
where error.code == NSFileReadNoSuchFileError && error.domain == NSCocoaErrorDomain {
|
||||||
|
|
||||||
|
let persistentStoreResult = self.addSQLiteStoreAndWait(
|
||||||
|
fileURL: fileURL,
|
||||||
|
configuration: configuration,
|
||||||
|
automigrating: false,
|
||||||
|
resetStoreOnMigrationFailure: false
|
||||||
|
)
|
||||||
|
|
||||||
|
completion(persistentStoreResult)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
|
||||||
|
CoreStore.handleError(
|
||||||
|
error as NSError,
|
||||||
|
"Failed to load SQLite \(typeName(NSPersistentStore)) metadata."
|
||||||
|
)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Migrates an SQLite store with the specified filename to the `DataStack`'s managed object model version WITHOUT adding the migrated store to the data stack.
|
||||||
|
|
||||||
|
- parameter fileName: the local filename for the SQLite persistent store in the "Application Support" directory.
|
||||||
|
- parameter configuration: an optional configuration name from the model file. If not specified, defaults to `nil` which indicates the "Default" configuration.
|
||||||
|
- parameter mappingModelBundles: an optional array of bundles to search mapping model files from. If not set, defaults to the `NSBundle.mainBundle()`.
|
||||||
|
- parameter sourceBundles: an optional array of bundles to search mapping model files from. If not set, defaults to the `NSBundle.mainBundle()`.
|
||||||
|
- returns: an `NSProgress` instance if a migration has started, or `nil` is no migrations are required
|
||||||
|
*/
|
||||||
|
public func upgradeSQLiteStoreIfNeeded(fileName fileName: String, configuration: String? = nil, mappingModelBundles: [NSBundle]? = nil, completion: (MigrationResult) -> Void) throws -> NSProgress? {
|
||||||
|
|
||||||
|
return try self.upgradeSQLiteStoreIfNeeded(
|
||||||
|
fileURL: applicationSupportDirectory.URLByAppendingPathComponent(
|
||||||
|
fileName,
|
||||||
|
isDirectory: false
|
||||||
|
),
|
||||||
|
configuration: configuration,
|
||||||
|
mappingModelBundles: mappingModelBundles,
|
||||||
|
completion: completion
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Migrates an SQLite store at the specified file URL and configuration name to the `DataStack`'s managed object model version. This method does NOT add the migrated store to the data stack.
|
||||||
|
|
||||||
|
- parameter fileName: the local filename for the SQLite persistent store in the "Application Support" directory.
|
||||||
|
- parameter configuration: an optional configuration name from the model file. If not specified, defaults to `nil` which indicates the "Default" configuration.
|
||||||
|
- parameter mappingModelBundles: an optional array of bundles to search mapping model files from. If not set, defaults to the `NSBundle.mainBundle()`.
|
||||||
|
- parameter sourceBundles: an optional array of bundles to search mapping model files from. If not set, defaults to the `NSBundle.mainBundle()`.
|
||||||
|
- returns: an `NSProgress` instance if a migration has started, or `nil` is no migrations are required
|
||||||
|
*/
|
||||||
|
public func upgradeSQLiteStoreIfNeeded(fileURL fileURL: NSURL = defaultSQLiteStoreURL, configuration: String? = nil, mappingModelBundles: [NSBundle]? = nil, completion: (MigrationResult) -> Void) throws -> NSProgress? {
|
||||||
|
|
||||||
|
let metadata: [String: AnyObject]
|
||||||
|
do {
|
||||||
|
|
||||||
|
metadata = try NSPersistentStoreCoordinator.metadataForPersistentStoreOfType(
|
||||||
|
NSSQLiteStoreType,
|
||||||
|
URL: fileURL
|
||||||
|
)
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
|
||||||
|
CoreStore.handleError(
|
||||||
|
error as NSError,
|
||||||
|
"Failed to load SQLite \(typeName(NSPersistentStore)) metadata."
|
||||||
|
)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.upgradeSQLiteStoreIfNeeded(
|
||||||
|
fileURL: fileURL,
|
||||||
|
metadata: metadata,
|
||||||
|
configuration: configuration,
|
||||||
|
mappingModelBundles: mappingModelBundles,
|
||||||
|
completion: completion
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Checks for the required migrations needed for the store with the specified filename and configuration to be migrated to the `DataStack`'s managed object model version. This method throws an error if the store does not exist, if inspection of the store failed, or no mapping model was found/inferred.
|
||||||
|
|
||||||
- parameter fileName: the local filename for the SQLite persistent store in the "Application Support" directory.
|
- parameter fileName: the local filename for the SQLite persistent store in the "Application Support" directory.
|
||||||
- parameter configuration: an optional configuration name from the model file. If not specified, defaults to `nil` which indicates the "Default" configuration.
|
- parameter configuration: an optional configuration name from the model file. If not specified, defaults to `nil` which indicates the "Default" configuration.
|
||||||
- parameter mappingModelBundles: an optional array of bundles to search mapping model files from. If not set, defaults to the `NSBundle.allBundles()`.
|
- parameter mappingModelBundles: an optional array of bundles to search mapping model files from. If not set, defaults to the `NSBundle.allBundles()`.
|
||||||
:return: a `MigrationType` indicating the type of migration required for the store; or `nil` if either inspection of the store failed, or no mapping model was found/inferred. `MigrationType` acts as a `Bool` and evaluates to `false` if no migration is required, and `true` if either a lightweight or custom migration is needed.
|
:return: an array of `MigrationType`s indicating the chain of migrations required for the store; or `nil` if either inspection of the store failed, or no mapping model was found/inferred. `MigrationType` acts as a `Bool` and evaluates to `false` if no migration is required, and `true` if either a lightweight or custom migration is needed.
|
||||||
*/
|
*/
|
||||||
public func needsMigrationForSQLiteStore(fileName fileName: String, configuration: String? = nil, mappingModelBundles: [NSBundle] = NSBundle.allBundles() as [NSBundle]) -> MigrationType? {
|
public func requiredMigrationsForSQLiteStore(fileName fileName: String, configuration: String? = nil, mappingModelBundles: [NSBundle] = NSBundle.allBundles() as [NSBundle]) throws -> [MigrationType] {
|
||||||
|
|
||||||
return needsMigrationForSQLiteStore(
|
return try requiredMigrationsForSQLiteStore(
|
||||||
fileURL: applicationSupportDirectory.URLByAppendingPathComponent(
|
fileURL: applicationSupportDirectory.URLByAppendingPathComponent(
|
||||||
fileName,
|
fileName,
|
||||||
isDirectory: false
|
isDirectory: false
|
||||||
@@ -60,7 +245,7 @@ public extension DataStack {
|
|||||||
- parameter mappingModelBundles: an optional array of bundles to search mapping model files from. If not set, defaults to the `NSBundle.allBundles()`.
|
- parameter mappingModelBundles: an optional array of bundles to search mapping model files from. If not set, defaults to the `NSBundle.allBundles()`.
|
||||||
:return: a `MigrationType` indicating the type of migration required for the store; or `nil` if either inspection of the store failed, or no mapping model was found/inferred. `MigrationType` acts as a `Bool` and evaluates to `false` if no migration is required, and `true` if either a lightweight or custom migration is needed.
|
:return: a `MigrationType` indicating the type of migration required for the store; or `nil` if either inspection of the store failed, or no mapping model was found/inferred. `MigrationType` acts as a `Bool` and evaluates to `false` if no migration is required, and `true` if either a lightweight or custom migration is needed.
|
||||||
*/
|
*/
|
||||||
public func needsMigrationForSQLiteStore(fileURL fileURL: NSURL = defaultSQLiteStoreURL, configuration: String? = nil, mappingModelBundles: [NSBundle] = NSBundle.allBundles() as [NSBundle]) -> MigrationType? {
|
public func requiredMigrationsForSQLiteStore(fileURL fileURL: NSURL = defaultSQLiteStoreURL, configuration: String? = nil, mappingModelBundles: [NSBundle] = NSBundle.allBundles() as [NSBundle]) throws -> [MigrationType] {
|
||||||
|
|
||||||
let metadata: [String : AnyObject]
|
let metadata: [String : AnyObject]
|
||||||
do {
|
do {
|
||||||
@@ -74,101 +259,30 @@ public extension DataStack {
|
|||||||
|
|
||||||
CoreStore.handleError(
|
CoreStore.handleError(
|
||||||
error as NSError,
|
error as NSError,
|
||||||
"Failed to add SQLite \(typeName(NSPersistentStore)) at \"\(fileURL)\"."
|
"Failed to load SQLite \(typeName(NSPersistentStore)) metadata."
|
||||||
)
|
)
|
||||||
return nil
|
throw error
|
||||||
}
|
}
|
||||||
|
|
||||||
let coordinator = self.coordinator;
|
guard let migrationSteps = self.computeMigrationFromStoreMetadata(metadata, configuration: configuration, mappingModelBundles: mappingModelBundles) else {
|
||||||
let destinationModel = coordinator.managedObjectModel
|
|
||||||
if destinationModel.isConfiguration(configuration, compatibleWithStoreMetadata: metadata) {
|
|
||||||
|
|
||||||
return .None
|
let error = NSError(coreStoreErrorCode: .MappingModelNotFound)
|
||||||
}
|
|
||||||
|
|
||||||
guard let sourceModel = NSManagedObjectModel(byMergingModels: [destinationModel], forStoreMetadata: metadata) else {
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if let _ = NSMappingModel(
|
|
||||||
fromBundles: mappingModelBundles,
|
|
||||||
forSourceModel: sourceModel,
|
|
||||||
destinationModel: destinationModel) {
|
|
||||||
|
|
||||||
return .Heavyweight
|
|
||||||
}
|
|
||||||
|
|
||||||
do {
|
|
||||||
|
|
||||||
try NSMappingModel.inferredMappingModelForSourceModel(
|
|
||||||
sourceModel,
|
|
||||||
destinationModel: destinationModel
|
|
||||||
)
|
|
||||||
|
|
||||||
return .Lightweight
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Migrates an SQLite store with the specified filename to the `DataStack`'s managed object model version. This method does NOT add the migrated store to the data stack.
|
|
||||||
|
|
||||||
- parameter fileName: the local filename for the SQLite persistent store in the "Application Support" directory.
|
|
||||||
- parameter configuration: an optional configuration name from the model file. If not specified, defaults to `nil` which indicates the "Default" configuration.
|
|
||||||
- parameter sourceBundles: an optional array of bundles to search mapping model files from. If not set, defaults to the `NSBundle.mainBundle()`.
|
|
||||||
- parameter sourceBundles: an optional array of bundles to search mapping model files from. If not set, defaults to the `NSBundle.mainBundle()`.
|
|
||||||
*/
|
|
||||||
public func upgradeSQLiteStoreIfNeeded(fileName fileName: String, configuration: String? = nil, sourceBundles: [NSBundle]? = nil, completion: (MigrationResult) -> Void) -> MigrationType? {
|
|
||||||
|
|
||||||
return self.upgradeSQLiteStoreIfNeeded(
|
|
||||||
fileURL: applicationSupportDirectory.URLByAppendingPathComponent(
|
|
||||||
fileName,
|
|
||||||
isDirectory: false
|
|
||||||
),
|
|
||||||
configuration: configuration,
|
|
||||||
sourceBundles: sourceBundles,
|
|
||||||
completion: completion
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Migrates an SQLite store at the specified file URL and configuration name to the `DataStack`'s managed object model version. This method does NOT add the migrated store to the data stack.
|
|
||||||
|
|
||||||
- parameter fileName: the local filename for the SQLite persistent store in the "Application Support" directory.
|
|
||||||
- parameter configuration: an optional configuration name from the model file. If not specified, defaults to `nil` which indicates the "Default" configuration.
|
|
||||||
- parameter sourceBundles: an optional array of bundles to search mapping model files from. If not set, defaults to the `NSBundle.mainBundle()`.
|
|
||||||
- parameter sourceBundles: an optional array of bundles to search mapping model files from. If not set, defaults to the `NSBundle.mainBundle()`.
|
|
||||||
*/
|
|
||||||
public func upgradeSQLiteStoreIfNeeded(fileURL fileURL: NSURL = defaultSQLiteStoreURL, configuration: String? = nil, sourceBundles: [NSBundle]? = nil, completion: (MigrationResult) -> Void) -> MigrationType? {
|
|
||||||
|
|
||||||
let metadata: [String: AnyObject]
|
|
||||||
do {
|
|
||||||
|
|
||||||
metadata = try NSPersistentStoreCoordinator.metadataForPersistentStoreOfType(
|
|
||||||
NSSQLiteStoreType,
|
|
||||||
URL: fileURL
|
|
||||||
)
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
|
|
||||||
let metadataError = error as NSError
|
|
||||||
CoreStore.handleError(
|
CoreStore.handleError(
|
||||||
metadataError,
|
error,
|
||||||
"Failed to load SQLite \(typeName(NSPersistentStore)) metadata at \"\(fileURL)\"."
|
"Failed to find migration steps from the store at URL \"\(fileURL)\" to version model \"\(self.modelVersion)\"."
|
||||||
)
|
)
|
||||||
|
throw error
|
||||||
GCDQueue.Main.async {
|
|
||||||
|
|
||||||
completion(MigrationResult(metadataError))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let migrationSteps = self.computeMigrationFromStoreMetadata(metadata, configuration: configuration, sourceBundles: sourceBundles) else {
|
return migrationSteps.map { $0.migrationType }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: Private
|
||||||
|
|
||||||
|
private func upgradeSQLiteStoreIfNeeded(fileURL fileURL: NSURL, metadata: [String: AnyObject], configuration: String?, mappingModelBundles: [NSBundle]?, completion: (MigrationResult) -> Void) -> NSProgress? {
|
||||||
|
|
||||||
|
guard let migrationSteps = self.computeMigrationFromStoreMetadata(metadata, configuration: configuration, mappingModelBundles: mappingModelBundles) else {
|
||||||
|
|
||||||
CoreStore.handleError(
|
CoreStore.handleError(
|
||||||
NSError(coreStoreErrorCode: .MappingModelNotFound),
|
NSError(coreStoreErrorCode: .MappingModelNotFound),
|
||||||
@@ -182,30 +296,28 @@ public extension DataStack {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if migrationSteps.count == 0 {
|
let numberOfMigrations: Int64 = Int64(migrationSteps.count)
|
||||||
|
if numberOfMigrations == 0 {
|
||||||
|
|
||||||
GCDQueue.Main.async {
|
GCDQueue.Main.async {
|
||||||
|
|
||||||
completion(MigrationResult(.None))
|
completion(MigrationResult([]))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
return .None
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var mergedMigrationType = MigrationType.None
|
let migrationTypes = migrationSteps.map { $0.migrationType }
|
||||||
var migrationResult: MigrationResult?
|
var migrationResult: MigrationResult?
|
||||||
|
|
||||||
var operations = [NSOperation]()
|
var operations = [NSOperation]()
|
||||||
var cancelled = false
|
var cancelled = false
|
||||||
for (sourceModel, destinationModel, mappingModel, migrationType) in migrationSteps {
|
|
||||||
|
let progress = NSProgress(totalUnitCount: numberOfMigrations)
|
||||||
|
|
||||||
|
for (sourceModel, destinationModel, mappingModel, _) in migrationSteps {
|
||||||
|
|
||||||
switch (mergedMigrationType, migrationType) {
|
progress.becomeCurrentWithPendingUnitCount(1)
|
||||||
|
let childProgress = NSProgress(totalUnitCount: 100)
|
||||||
case (.None, _), (.Lightweight, .Heavyweight):
|
|
||||||
mergedMigrationType = migrationType
|
|
||||||
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
operations.append(
|
operations.append(
|
||||||
NSBlockOperation { [weak self] in
|
NSBlockOperation { [weak self] in
|
||||||
@@ -223,8 +335,10 @@ public extension DataStack {
|
|||||||
fileURL: fileURL,
|
fileURL: fileURL,
|
||||||
sourceModel: sourceModel,
|
sourceModel: sourceModel,
|
||||||
destinationModel: destinationModel,
|
destinationModel: destinationModel,
|
||||||
mappingModel: mappingModel
|
mappingModel: mappingModel,
|
||||||
|
progress: childProgress
|
||||||
)
|
)
|
||||||
|
childProgress.setProgressHandler(nil)
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
|
|
||||||
@@ -232,8 +346,16 @@ public extension DataStack {
|
|||||||
cancelled = true
|
cancelled = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GCDQueue.Main.async {
|
||||||
|
|
||||||
|
withExtendedLifetime(childProgress) { (_: NSProgress) -> Void in }
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
progress.resignCurrent()
|
||||||
}
|
}
|
||||||
|
|
||||||
let migrationOperation = NSBlockOperation()
|
let migrationOperation = NSBlockOperation()
|
||||||
@@ -243,7 +365,9 @@ public extension DataStack {
|
|||||||
|
|
||||||
GCDQueue.Main.async {
|
GCDQueue.Main.async {
|
||||||
|
|
||||||
completion(migrationResult ?? MigrationResult(mergedMigrationType))
|
completion(migrationResult ?? MigrationResult(migrationTypes))
|
||||||
|
|
||||||
|
withExtendedLifetime(progress) { (_: NSProgress) -> Void in }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -252,105 +376,10 @@ public extension DataStack {
|
|||||||
|
|
||||||
self.migrationQueue.addOperations(operations, waitUntilFinished: false)
|
self.migrationQueue.addOperations(operations, waitUntilFinished: false)
|
||||||
|
|
||||||
return mergedMigrationType
|
return progress
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private func computeMigrationFromStoreMetadata(metadata: [String: AnyObject], configuration: String? = nil, mappingModelBundles: [NSBundle]? = nil) -> [(sourceModel: NSManagedObjectModel, destinationModel: NSManagedObjectModel, mappingModel: NSMappingModel, migrationType: MigrationType)]? {
|
||||||
Asynchronously adds to the stack an SQLite store from the given SQLite file name. Note that using `addSQLiteStore(...)` instead of `addSQLiteStoreAndWait(...)` implies that the migrations are allowed and expected (thus the asynchronous `completion`.)
|
|
||||||
|
|
||||||
- parameter fileName: the local filename for the SQLite persistent store in the "Application Support" directory. A new SQLite file will be created if it does not exist. Note that if you have multiple configurations, you will need to specify a different `fileName` explicitly for each of them.
|
|
||||||
- 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 `fileName` explicitly for each of them.
|
|
||||||
- parameter completion: the closure to be executed on the main queue when the process completes, either due to success or failure. The closure's `PersistentStoreResult` argument indicates the result.
|
|
||||||
*/
|
|
||||||
public func addSQLiteStore(fileName fileName: String, configuration: String? = nil, sourceBundles: [NSBundle]? = nil, completion: (PersistentStoreResult) -> Void) {
|
|
||||||
|
|
||||||
self.addSQLiteStore(
|
|
||||||
fileURL: applicationSupportDirectory.URLByAppendingPathComponent(
|
|
||||||
fileName,
|
|
||||||
isDirectory: false
|
|
||||||
),
|
|
||||||
configuration: configuration,
|
|
||||||
sourceBundles: sourceBundles,
|
|
||||||
completion: completion
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Asynchronously adds to the stack an SQLite store from the given SQLite file URL. Note that using `addSQLiteStore(...)` instead of `addSQLiteStoreAndWait(...)` implies that the migrations are allowed and expected (thus the asynchronous `completion`.)
|
|
||||||
|
|
||||||
- parameter fileURL: the local file URL for the SQLite persistent store. A new SQLite file will be created if it does not exist. If not specified, defaults to a file URL pointing to a "<Application name>.sqlite" file in the "Application Support" directory. Note that if you have multiple configurations, you will need to specify a different `fileURL` explicitly for each of them.
|
|
||||||
- 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 `fileURL` explicitly for each of them.
|
|
||||||
- parameter completion: the closure to be executed on the main queue when the process completes, either due to success or failure. The closure's `PersistentStoreResult` argument indicates the result.
|
|
||||||
*/
|
|
||||||
public func addSQLiteStore(fileURL fileURL: NSURL = defaultSQLiteStoreURL, configuration: String? = nil, sourceBundles: [NSBundle]? = NSBundle.allBundles(), completion: (PersistentStoreResult) -> Void) {
|
|
||||||
|
|
||||||
let coordinator = self.coordinator;
|
|
||||||
if let store = coordinator.persistentStoreForURL(fileURL) {
|
|
||||||
|
|
||||||
let isExistingStoreAutomigrating = store.options?[NSMigratePersistentStoresAutomaticallyOption] as? Bool == true
|
|
||||||
|
|
||||||
if store.type == NSSQLiteStoreType
|
|
||||||
&& isExistingStoreAutomigrating
|
|
||||||
&& store.configurationName == (configuration ?? Into.defaultConfigurationName) {
|
|
||||||
|
|
||||||
GCDQueue.Main.async {
|
|
||||||
|
|
||||||
completion(PersistentStoreResult(store))
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
CoreStore.handleError(
|
|
||||||
NSError(coreStoreErrorCode: .DifferentPersistentStoreExistsAtURL),
|
|
||||||
"Failed to add SQLite \(typeName(NSPersistentStore)) at \"\(fileURL)\" because a different \(typeName(NSPersistentStore)) at that URL already exists."
|
|
||||||
)
|
|
||||||
|
|
||||||
GCDQueue.Main.async {
|
|
||||||
|
|
||||||
completion(PersistentStoreResult(.DifferentPersistentStoreExistsAtURL))
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
do {
|
|
||||||
|
|
||||||
try NSFileManager.defaultManager().createDirectoryAtURL(
|
|
||||||
fileURL.URLByDeletingLastPathComponent!,
|
|
||||||
withIntermediateDirectories: true,
|
|
||||||
attributes: nil
|
|
||||||
)
|
|
||||||
}
|
|
||||||
catch _ { }
|
|
||||||
|
|
||||||
self.upgradeSQLiteStoreIfNeeded(
|
|
||||||
fileURL: fileURL,
|
|
||||||
configuration: configuration,
|
|
||||||
sourceBundles: sourceBundles,
|
|
||||||
completion: { (result) -> Void in
|
|
||||||
|
|
||||||
if case .Failure(let error) = result
|
|
||||||
where error.domain != NSCocoaErrorDomain || error.code != NSFileReadNoSuchFileError {
|
|
||||||
|
|
||||||
completion(PersistentStoreResult(error))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let persistentStoreResult = self.addSQLiteStoreAndWait(
|
|
||||||
fileURL: fileURL,
|
|
||||||
configuration: configuration,
|
|
||||||
automigrating: false,
|
|
||||||
resetStoreOnMigrationFailure: false
|
|
||||||
)
|
|
||||||
|
|
||||||
completion(persistentStoreResult)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// MARK: Private
|
|
||||||
|
|
||||||
private func computeMigrationFromStoreMetadata(metadata: [String: AnyObject], configuration: String? = nil, sourceBundles: [NSBundle]? = nil) -> [(sourceModel: NSManagedObjectModel, destinationModel: NSManagedObjectModel, mappingModel: NSMappingModel, migrationType: MigrationType)]? {
|
|
||||||
|
|
||||||
let model = self.model
|
let model = self.model
|
||||||
if model.isConfiguration(configuration, compatibleWithStoreMetadata: metadata) {
|
if model.isConfiguration(configuration, compatibleWithStoreMetadata: metadata) {
|
||||||
@@ -358,42 +387,16 @@ public extension DataStack {
|
|||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
let metadataModel = NSManagedObjectModel(byMergingModels: model.mergedModels(), forStoreMetadata: metadata)!
|
guard let initialModel = model[metadata],
|
||||||
if let bypassModel = NSMappingModel(
|
var currentVersion = initialModel.currentModelVersion else {
|
||||||
fromBundles: sourceBundles,
|
|
||||||
forSourceModel: metadataModel,
|
|
||||||
destinationModel: model) {
|
|
||||||
|
|
||||||
return [
|
return nil
|
||||||
(
|
|
||||||
sourceModel: metadataModel,
|
|
||||||
destinationModel: model,
|
|
||||||
mappingModel: bypassModel,
|
|
||||||
migrationType: .Heavyweight
|
|
||||||
)
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var initialModel: NSManagedObjectModel?
|
let migrationChain: MigrationChain = self.migrationChain.empty
|
||||||
if let modelHashes = metadata[NSStoreModelVersionHashesKey] as? [String : NSData],
|
? [currentVersion: model.currentModelVersion!]
|
||||||
let modelVersions = model.modelVersions {
|
: self.migrationChain
|
||||||
|
|
||||||
for modelVersion in modelVersions {
|
|
||||||
|
|
||||||
if let versionModel = model[modelVersion] where modelHashes == versionModel.entityVersionHashesByName {
|
|
||||||
|
|
||||||
initialModel = versionModel
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
guard var currentVersion = initialModel?.currentModelVersion else {
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
let migrationChain = self.migrationChain
|
|
||||||
var migrationSteps = [(sourceModel: NSManagedObjectModel, destinationModel: NSManagedObjectModel, mappingModel: NSMappingModel, migrationType: MigrationType)]()
|
var migrationSteps = [(sourceModel: NSManagedObjectModel, destinationModel: NSManagedObjectModel, mappingModel: NSMappingModel, migrationType: MigrationType)]()
|
||||||
|
|
||||||
while let nextVersion = migrationChain.nextVersionFrom(currentVersion),
|
while let nextVersion = migrationChain.nextVersionFrom(currentVersion),
|
||||||
@@ -401,7 +404,7 @@ public extension DataStack {
|
|||||||
let destinationModel = model[nextVersion] {
|
let destinationModel = model[nextVersion] {
|
||||||
|
|
||||||
if let mappingModel = NSMappingModel(
|
if let mappingModel = NSMappingModel(
|
||||||
fromBundles: sourceBundles,
|
fromBundles: mappingModelBundles,
|
||||||
forSourceModel: sourceModel,
|
forSourceModel: sourceModel,
|
||||||
destinationModel: destinationModel) {
|
destinationModel: destinationModel) {
|
||||||
|
|
||||||
@@ -410,7 +413,10 @@ public extension DataStack {
|
|||||||
sourceModel: sourceModel,
|
sourceModel: sourceModel,
|
||||||
destinationModel: destinationModel,
|
destinationModel: destinationModel,
|
||||||
mappingModel: mappingModel,
|
mappingModel: mappingModel,
|
||||||
migrationType: .Heavyweight
|
migrationType: .Heavyweight(
|
||||||
|
sourceVersion: currentVersion,
|
||||||
|
destinationVersion: nextVersion
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -428,7 +434,10 @@ public extension DataStack {
|
|||||||
sourceModel: sourceModel,
|
sourceModel: sourceModel,
|
||||||
destinationModel: destinationModel,
|
destinationModel: destinationModel,
|
||||||
mappingModel: mappingModel,
|
mappingModel: mappingModel,
|
||||||
migrationType: .Lightweight
|
migrationType: .Lightweight(
|
||||||
|
sourceVersion: currentVersion,
|
||||||
|
destinationVersion: nextVersion
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -448,7 +457,7 @@ public extension DataStack {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
private func startMigrationForSQLiteStore(fileURL fileURL: NSURL, sourceModel: NSManagedObjectModel, destinationModel: NSManagedObjectModel, mappingModel: NSMappingModel) throws {
|
private func startMigrationForSQLiteStore(fileURL fileURL: NSURL, sourceModel: NSManagedObjectModel, destinationModel: NSManagedObjectModel, mappingModel: NSMappingModel, progress: NSProgress) throws {
|
||||||
|
|
||||||
autoreleasepool {
|
autoreleasepool {
|
||||||
|
|
||||||
@@ -462,25 +471,10 @@ public extension DataStack {
|
|||||||
try! journalUpdatingCoordinator.removePersistentStore(store)
|
try! journalUpdatingCoordinator.removePersistentStore(store)
|
||||||
}
|
}
|
||||||
|
|
||||||
let migrationManager = NSMigrationManager(
|
let migrationManager = MigrationManager(
|
||||||
sourceModel: sourceModel,
|
sourceModel: sourceModel,
|
||||||
destinationModel: destinationModel
|
destinationModel: destinationModel,
|
||||||
)
|
progress: progress
|
||||||
|
|
||||||
var lastReportedProgress: Float = -1
|
|
||||||
let timer = GCDTimer.createSuspended(
|
|
||||||
.Main,
|
|
||||||
interval: 0.1,
|
|
||||||
eventHandler: { (timer) -> Void in
|
|
||||||
|
|
||||||
let progress = migrationManager.migrationProgress
|
|
||||||
if progress > lastReportedProgress {
|
|
||||||
|
|
||||||
// TODO: progress
|
|
||||||
CoreStore.log(.Trace, message: "migration progress: \(progress)")
|
|
||||||
lastReportedProgress = progress
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
let temporaryDirectoryURL = NSURL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true).URLByAppendingPathComponent(NSProcessInfo().globallyUniqueString)
|
let temporaryDirectoryURL = NSURL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true).URLByAppendingPathComponent(NSProcessInfo().globallyUniqueString)
|
||||||
@@ -493,6 +487,7 @@ public extension DataStack {
|
|||||||
)
|
)
|
||||||
|
|
||||||
let temporaryFileURL = temporaryDirectoryURL.URLByAppendingPathComponent(fileURL.lastPathComponent!, isDirectory: false)
|
let temporaryFileURL = temporaryDirectoryURL.URLByAppendingPathComponent(fileURL.lastPathComponent!, isDirectory: false)
|
||||||
|
|
||||||
do {
|
do {
|
||||||
|
|
||||||
try migrationManager.migrateStoreFromURL(
|
try migrationManager.migrateStoreFromURL(
|
||||||
@@ -507,25 +502,22 @@ public extension DataStack {
|
|||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
|
|
||||||
timer.suspend()
|
|
||||||
|
|
||||||
do {
|
do {
|
||||||
|
|
||||||
try fileManager.removeItemAtURL(temporaryDirectoryURL)
|
try fileManager.removeItemAtURL(temporaryDirectoryURL)
|
||||||
}
|
}
|
||||||
catch _ { }
|
catch _ { }
|
||||||
|
|
||||||
let migrationError = error as NSError
|
let sourceVersion = migrationManager.sourceModel.currentModelVersion ?? "???"
|
||||||
|
let destinationVersion = migrationManager.destinationModel.currentModelVersion ?? "???"
|
||||||
CoreStore.handleError(
|
CoreStore.handleError(
|
||||||
migrationError,
|
error as NSError,
|
||||||
"Failed to migrate from version model \"\(migrationManager.sourceModel)\" to version model \"\(migrationManager.destinationModel)\"."
|
"Failed to migrate from version model \"\(sourceVersion)\" to version model \"\(destinationVersion)\"."
|
||||||
)
|
)
|
||||||
|
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
|
||||||
timer.suspend()
|
|
||||||
|
|
||||||
do {
|
do {
|
||||||
|
|
||||||
try fileManager.replaceItemAtURL(
|
try fileManager.replaceItemAtURL(
|
||||||
@@ -536,6 +528,8 @@ public extension DataStack {
|
|||||||
resultingItemURL: nil
|
resultingItemURL: nil
|
||||||
)
|
)
|
||||||
|
|
||||||
|
progress.completedUnitCount = progress.totalUnitCount
|
||||||
|
|
||||||
do {
|
do {
|
||||||
|
|
||||||
try fileManager.removeItemAtPath(fileURL.path! + "-shm")
|
try fileManager.removeItemAtPath(fileURL.path! + "-shm")
|
||||||
@@ -550,10 +544,11 @@ public extension DataStack {
|
|||||||
}
|
}
|
||||||
catch _ { }
|
catch _ { }
|
||||||
|
|
||||||
let replaceError = error as NSError
|
let sourceVersion = migrationManager.sourceModel.currentModelVersion ?? "???"
|
||||||
|
let destinationVersion = migrationManager.destinationModel.currentModelVersion ?? "???"
|
||||||
CoreStore.handleError(
|
CoreStore.handleError(
|
||||||
replaceError,
|
error as NSError,
|
||||||
"Failed to save store after migrating from version model \"\(migrationManager.sourceModel)\" to version model \"\(migrationManager.destinationModel)\"."
|
"Failed to save store after migrating from version model \"\(sourceVersion)\" to version model \"\(destinationVersion)\"."
|
||||||
)
|
)
|
||||||
|
|
||||||
throw error
|
throw error
|
||||||
|
|||||||
@@ -137,6 +137,11 @@ public struct MigrationChain: NilLiteralConvertible, StringLiteralConvertible, D
|
|||||||
internal let leafVersions: Set<String>
|
internal let leafVersions: Set<String>
|
||||||
internal let valid: Bool
|
internal let valid: Bool
|
||||||
|
|
||||||
|
internal var empty: Bool {
|
||||||
|
|
||||||
|
return self.versionTree.count <= 0
|
||||||
|
}
|
||||||
|
|
||||||
internal func contains(version: String) -> Bool {
|
internal func contains(version: String) -> Bool {
|
||||||
|
|
||||||
return self.rootVersions.contains(version)
|
return self.rootVersions.contains(version)
|
||||||
|
|||||||
@@ -38,17 +38,53 @@ public enum MigrationType: BooleanType {
|
|||||||
/**
|
/**
|
||||||
Indicates that the persistent store matches the latest model version and no migration is needed
|
Indicates that the persistent store matches the latest model version and no migration is needed
|
||||||
*/
|
*/
|
||||||
case None
|
case None(version: String)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Indicates that the persistent store does not match the latest model version but Core Data can infer the mapping model, so a lightweight migration is needed
|
Indicates that the persistent store does not match the latest model version but Core Data can infer the mapping model, so a lightweight migration is needed
|
||||||
*/
|
*/
|
||||||
case Lightweight
|
case Lightweight(sourceVersion: String, destinationVersion: String)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Indicates that the persistent store does not match the latest model version and Core Data could not infer a mapping model, so a custom migration is needed
|
Indicates that the persistent store does not match the latest model version and Core Data could not infer a mapping model, so a custom migration is needed
|
||||||
*/
|
*/
|
||||||
case Heavyweight
|
case Heavyweight(sourceVersion: String, destinationVersion: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
Returns the source model version for the migration type. If no migration is required, `sourceVersion` will be equal to the `destinationVersion`.
|
||||||
|
*/
|
||||||
|
public var sourceVersion: String {
|
||||||
|
|
||||||
|
switch self {
|
||||||
|
|
||||||
|
case .None(let version):
|
||||||
|
return version
|
||||||
|
|
||||||
|
case .Lightweight(let sourceVersion, _):
|
||||||
|
return sourceVersion
|
||||||
|
|
||||||
|
case .Heavyweight(let sourceVersion, _):
|
||||||
|
return sourceVersion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Returns the destination model version for the migration type. If no migration is required, `destinationVersion` will be equal to the `sourceVersion`.
|
||||||
|
*/
|
||||||
|
public var destinationVersion: String {
|
||||||
|
|
||||||
|
switch self {
|
||||||
|
|
||||||
|
case .None(let version):
|
||||||
|
return version
|
||||||
|
|
||||||
|
case .Lightweight(_, let destinationVersion):
|
||||||
|
return destinationVersion
|
||||||
|
|
||||||
|
case .Heavyweight(_, let destinationVersion):
|
||||||
|
return destinationVersion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// MARK: BooleanType
|
// MARK: BooleanType
|
||||||
@@ -103,7 +139,7 @@ public enum MigrationResult {
|
|||||||
/**
|
/**
|
||||||
`MigrationResult.Success` indicates that the `commit()` for the transaction succeeded, either because the save succeeded or because there were no changes to save. The associated value `hasChanges` indicates if there were saved changes or not.
|
`MigrationResult.Success` indicates that the `commit()` for the transaction succeeded, either because the save succeeded or because there were no changes to save. The associated value `hasChanges` indicates if there were saved changes or not.
|
||||||
*/
|
*/
|
||||||
case Success(MigrationType)
|
case Success([MigrationType])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
`SaveResult.Failure` indicates that the `commit()` for the transaction failed. The associated object for this value is the related `NSError` instance.
|
`SaveResult.Failure` indicates that the `commit()` for the transaction failed. The associated object for this value is the related `NSError` instance.
|
||||||
@@ -113,9 +149,9 @@ public enum MigrationResult {
|
|||||||
|
|
||||||
// MARK: Internal
|
// MARK: Internal
|
||||||
|
|
||||||
internal init(_ migrationType: MigrationType) {
|
internal init(_ migrationTypes: [MigrationType]) {
|
||||||
|
|
||||||
self = .Success(migrationType)
|
self = .Success(migrationTypes)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal init(_ error: NSError) {
|
internal init(_ error: NSError) {
|
||||||
|
|||||||
@@ -70,10 +70,10 @@ public /*abstract*/ class BaseDataTransaction {
|
|||||||
return object
|
return object
|
||||||
|
|
||||||
case (.None, true):
|
case (.None, true):
|
||||||
CoreStore.fatalError("Attempted to create an entity of type \(typeName(entityClass)) with ambiguous destination persistent store, but the configuration name was not specified.")
|
fatalError("Attempted to create an entity of type \(typeName(entityClass)) with ambiguous destination persistent store, but the configuration name was not specified.")
|
||||||
|
|
||||||
default:
|
default:
|
||||||
CoreStore.fatalError("Attempted to create an entity of type \(typeName(entityClass)), but a destination persistent store containing the entity type could not be found.")
|
fatalError("Attempted to create an entity of type \(typeName(entityClass)), but a destination persistent store containing the entity type could not be found.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -88,11 +88,11 @@ public /*abstract*/ class BaseDataTransaction {
|
|||||||
default:
|
default:
|
||||||
if let configuration = into.configuration {
|
if let configuration = into.configuration {
|
||||||
|
|
||||||
CoreStore.fatalError("Attempted to create an entity of type \(typeName(entityClass)) into the configuration \"\(configuration)\", which it doesn't belong to.")
|
fatalError("Attempted to create an entity of type \(typeName(entityClass)) into the configuration \"\(configuration)\", which it doesn't belong to.")
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|
||||||
CoreStore.fatalError("Attempted to create an entity of type \(typeName(entityClass)) into the default configuration, which it doesn't belong to.")
|
fatalError("Attempted to create an entity of type \(typeName(entityClass)) into the default configuration, which it doesn't belong to.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ public final class DataStack {
|
|||||||
/**
|
/**
|
||||||
Initializes a `DataStack` from an `NSManagedObjectModel`.
|
Initializes a `DataStack` from an `NSManagedObjectModel`.
|
||||||
|
|
||||||
- parameter modelName: the name of the (.xcdatamodeld) model file. If not specified, the application name will be used
|
- parameter modelName: the name of the (.xcdatamodeld) model file. If not specified, the application name will be used.
|
||||||
- parameter bundle: an optional bundle to load models from. If not specified, the main bundle will be used.
|
- parameter bundle: an optional bundle to load models from. If not specified, the main bundle will be used.
|
||||||
- parameter migrationChain: the `MigrationChain` that indicates the heirarchy of the model's version names. If not specified, will default to a non-migrating data stack.
|
- parameter migrationChain: the `MigrationChain` that indicates the heirarchy of the model's version names. If not specified, will default to a non-migrating data stack.
|
||||||
*/
|
*/
|
||||||
@@ -74,6 +74,14 @@ public final class DataStack {
|
|||||||
self.rootSavingContext.parentStack = self
|
self.rootSavingContext.parentStack = self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Returns the `DataStack`'s model version. The version string is the same as the name of the .xcdatamodeld file.
|
||||||
|
*/
|
||||||
|
public var modelVersion: String {
|
||||||
|
|
||||||
|
return self.model.currentModelVersion!
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Adds an in-memory store to the stack.
|
Adds an in-memory store to the stack.
|
||||||
|
|
||||||
@@ -160,6 +168,11 @@ public final class DataStack {
|
|||||||
*/
|
*/
|
||||||
public func addSQLiteStoreAndWait(fileURL fileURL: NSURL = defaultSQLiteStoreURL, configuration: String? = nil, automigrating: Bool = true, resetStoreOnMigrationFailure: Bool = false) -> PersistentStoreResult {
|
public func addSQLiteStoreAndWait(fileURL fileURL: NSURL = defaultSQLiteStoreURL, configuration: String? = nil, automigrating: Bool = true, resetStoreOnMigrationFailure: Bool = false) -> PersistentStoreResult {
|
||||||
|
|
||||||
|
CoreStore.assert(
|
||||||
|
fileURL.fileURL,
|
||||||
|
"The specified file URL for the SQLite store is invalid: \"\(fileURL)\""
|
||||||
|
)
|
||||||
|
|
||||||
let coordinator = self.coordinator;
|
let coordinator = self.coordinator;
|
||||||
if let store = coordinator.persistentStoreForURL(fileURL) {
|
if let store = coordinator.persistentStoreForURL(fileURL) {
|
||||||
|
|
||||||
|
|||||||
@@ -136,31 +136,54 @@
|
|||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Organism" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="C06-r4-27K">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Organism" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="C06-r4-27K">
|
||||||
<rect key="frame" x="20" y="20" width="560" height="26.5"/>
|
<rect key="frame" x="20" y="90" width="560" height="26.5"/>
|
||||||
<fontDescription key="fontDescription" name="HelveticaNeue-Bold" family="Helvetica Neue" pointSize="22"/>
|
<fontDescription key="fontDescription" name="HelveticaNeue-Bold" family="Helvetica Neue" pointSize="22"/>
|
||||||
<color key="textColor" red="0.92549019610000005" green="0.94117647059999998" blue="0.94509803920000002" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
<color key="textColor" red="0.92549019610000005" green="0.94117647059999998" blue="0.94509803920000002" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</label>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="attributes" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="fXh-OL-4Qb">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="attributes" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="fXh-OL-4Qb">
|
||||||
<rect key="frame" x="20" y="70" width="560" height="20.5"/>
|
<rect key="frame" x="20" y="140" width="560" height="20.5"/>
|
||||||
<fontDescription key="fontDescription" name="HelveticaNeue-Light" family="Helvetica Neue" pointSize="17"/>
|
<fontDescription key="fontDescription" name="HelveticaNeue-Light" family="Helvetica Neue" pointSize="17"/>
|
||||||
<color key="textColor" red="0.92549019610000005" green="0.94117647059999998" blue="0.94509803920000002" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
<color key="textColor" red="0.92549019610000005" green="0.94117647059999998" blue="0.94509803920000002" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</label>
|
||||||
|
<segmentedControl opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="Hwa-fO-fC5">
|
||||||
|
<rect key="frame" x="20" y="20" width="560" height="29"/>
|
||||||
|
<segments>
|
||||||
|
<segment title="First"/>
|
||||||
|
<segment title="Second"/>
|
||||||
|
<segment title=""/>
|
||||||
|
</segments>
|
||||||
|
<color key="tintColor" cocoaTouchSystemColor="tableCellGroupedBackgroundColor"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="segmentedControlValueChanged:" destination="hJK-5I-1TQ" eventType="valueChanged" id="jNH-gx-mOg"/>
|
||||||
|
</connections>
|
||||||
|
</segmentedControl>
|
||||||
|
<progressView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="750" progress="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="H1P-2g-DHX">
|
||||||
|
<rect key="frame" x="20" y="68" width="560" height="2"/>
|
||||||
|
<color key="progressTintColor" red="1" green="1" blue="1" alpha="1" colorSpace="calibratedRGB"/>
|
||||||
|
<color key="trackTintColor" white="1" alpha="0.20000000000000001" colorSpace="calibratedWhite"/>
|
||||||
|
</progressView>
|
||||||
</subviews>
|
</subviews>
|
||||||
<color key="backgroundColor" red="0.20392156859999999" green="0.28627450980000002" blue="0.36862745099999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
<color key="backgroundColor" red="0.20392156859999999" green="0.28627450980000002" blue="0.36862745099999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
|
<constraint firstItem="H1P-2g-DHX" firstAttribute="leading" secondItem="5Hd-jr-1nW" secondAttribute="leading" constant="20" id="2Nf-U0-bTq"/>
|
||||||
<constraint firstItem="fXh-OL-4Qb" firstAttribute="top" secondItem="C06-r4-27K" secondAttribute="bottom" constant="23.5" id="6d1-A5-Zc5"/>
|
<constraint firstItem="fXh-OL-4Qb" firstAttribute="top" secondItem="C06-r4-27K" secondAttribute="bottom" constant="23.5" id="6d1-A5-Zc5"/>
|
||||||
<constraint firstAttribute="trailing" secondItem="C06-r4-27K" secondAttribute="trailing" constant="20" id="DC3-u7-f9M"/>
|
<constraint firstItem="Hwa-fO-fC5" firstAttribute="leading" secondItem="5Hd-jr-1nW" secondAttribute="leading" constant="20" id="CDZ-qV-3eK"/>
|
||||||
|
<constraint firstItem="H1P-2g-DHX" firstAttribute="top" secondItem="Hwa-fO-fC5" secondAttribute="bottom" constant="20" id="Dcs-Vv-Xfv"/>
|
||||||
<constraint firstItem="fXh-OL-4Qb" firstAttribute="leading" secondItem="C06-r4-27K" secondAttribute="leading" id="SnQ-Ra-KkU"/>
|
<constraint firstItem="fXh-OL-4Qb" firstAttribute="leading" secondItem="C06-r4-27K" secondAttribute="leading" id="SnQ-Ra-KkU"/>
|
||||||
<constraint firstItem="C06-r4-27K" firstAttribute="top" secondItem="5Hd-jr-1nW" secondAttribute="top" constant="20" id="abW-Z0-uln"/>
|
<constraint firstItem="Hwa-fO-fC5" firstAttribute="top" secondItem="5Hd-jr-1nW" secondAttribute="top" constant="20" id="Zo1-kW-kAw"/>
|
||||||
|
<constraint firstItem="C06-r4-27K" firstAttribute="top" secondItem="H1P-2g-DHX" secondAttribute="bottom" constant="20" id="bBW-Bn-oeN"/>
|
||||||
<constraint firstAttribute="trailing" secondItem="fXh-OL-4Qb" secondAttribute="trailing" constant="20" id="mhR-0c-i0n"/>
|
<constraint firstAttribute="trailing" secondItem="fXh-OL-4Qb" secondAttribute="trailing" constant="20" id="mhR-0c-i0n"/>
|
||||||
<constraint firstAttribute="trailing" secondItem="C06-r4-27K" secondAttribute="trailing" constant="20" id="s0c-XG-e70"/>
|
<constraint firstAttribute="trailing" secondItem="C06-r4-27K" secondAttribute="trailing" constant="20" id="s0c-XG-e70"/>
|
||||||
|
<constraint firstAttribute="trailing" secondItem="H1P-2g-DHX" secondAttribute="trailing" constant="20" id="swp-18-f5e"/>
|
||||||
|
<constraint firstItem="C06-r4-27K" firstAttribute="top" secondItem="Hwa-fO-fC5" secondAttribute="bottom" constant="20" id="tYt-wb-Lk8"/>
|
||||||
|
<constraint firstAttribute="trailing" secondItem="Hwa-fO-fC5" secondAttribute="trailing" constant="20" id="ujX-gH-Wlm"/>
|
||||||
<constraint firstItem="C06-r4-27K" firstAttribute="leading" secondItem="5Hd-jr-1nW" secondAttribute="leading" constant="20" id="yiG-c9-BZg"/>
|
<constraint firstItem="C06-r4-27K" firstAttribute="leading" secondItem="5Hd-jr-1nW" secondAttribute="leading" constant="20" id="yiG-c9-BZg"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
<variation key="default">
|
<variation key="default">
|
||||||
<mask key="constraints">
|
<mask key="constraints">
|
||||||
<exclude reference="DC3-u7-f9M"/>
|
<exclude reference="tYt-wb-Lk8"/>
|
||||||
</mask>
|
</mask>
|
||||||
</variation>
|
</variation>
|
||||||
</view>
|
</view>
|
||||||
@@ -206,6 +229,8 @@
|
|||||||
<simulatedToolbarMetrics key="simulatedBottomBarMetrics"/>
|
<simulatedToolbarMetrics key="simulatedBottomBarMetrics"/>
|
||||||
<connections>
|
<connections>
|
||||||
<outlet property="organismLabel" destination="fXh-OL-4Qb" id="shX-fK-haw"/>
|
<outlet property="organismLabel" destination="fXh-OL-4Qb" id="shX-fK-haw"/>
|
||||||
|
<outlet property="progressView" destination="H1P-2g-DHX" id="Asy-fE-gXI"/>
|
||||||
|
<outlet property="segmentedControl" destination="Hwa-fO-fC5" id="myL-tO-4ah"/>
|
||||||
<outlet property="titleLabel" destination="C06-r4-27K" id="zQr-YC-moe"/>
|
<outlet property="titleLabel" destination="C06-r4-27K" id="zQr-YC-moe"/>
|
||||||
</connections>
|
</connections>
|
||||||
</tableViewController>
|
</tableViewController>
|
||||||
|
|||||||
@@ -19,7 +19,39 @@ class MigrationsDemoViewController: UITableViewController {
|
|||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
|
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
self.selectModelVersion(self.models.first!)
|
|
||||||
|
let models = self.models
|
||||||
|
if let segmentedControl = self.segmentedControl {
|
||||||
|
|
||||||
|
for (index, model) in models.enumerate() {
|
||||||
|
|
||||||
|
segmentedControl.setTitle(
|
||||||
|
model.label,
|
||||||
|
forSegmentAtIndex: index
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let dataStack = DataStack(modelName: "MigrationDemo")
|
||||||
|
do {
|
||||||
|
|
||||||
|
let migrations = try dataStack.requiredMigrationsForSQLiteStore(
|
||||||
|
fileName: "MigrationDemo.sqlite"
|
||||||
|
)
|
||||||
|
|
||||||
|
let storeVersion = migrations.first?.sourceVersion ?? dataStack.modelVersion
|
||||||
|
for model in models {
|
||||||
|
|
||||||
|
if model.version == storeVersion {
|
||||||
|
|
||||||
|
self.selectModelVersion(model, animated: false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch _ { }
|
||||||
|
|
||||||
|
self.selectModelVersion(self.models.first!, animated: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -47,21 +79,23 @@ class MigrationsDemoViewController: UITableViewController {
|
|||||||
|
|
||||||
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
|
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
|
||||||
|
|
||||||
self.selectModelVersion(self.models[indexPath.row])
|
self.selectModelVersion(self.models[indexPath.row], animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// MARK: Private
|
// MARK: Private
|
||||||
|
|
||||||
private typealias ModelMetadata = (version: String, entityType: AnyClass, migrationChain: MigrationChain)
|
private typealias ModelMetadata = (label: String, version: String, entityType: AnyClass, migrationChain: MigrationChain)
|
||||||
|
|
||||||
private let models: [ModelMetadata] = [
|
private let models: [ModelMetadata] = [
|
||||||
(
|
(
|
||||||
|
label: "Model V1",
|
||||||
version: "MigrationDemo",
|
version: "MigrationDemo",
|
||||||
entityType: OrganismV1.self,
|
entityType: OrganismV1.self,
|
||||||
migrationChain: ["MigrationDemoV3", "MigrationDemoV2", "MigrationDemo"]
|
migrationChain: ["MigrationDemoV3", "MigrationDemoV2", "MigrationDemo"]
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
|
label: "Model V2",
|
||||||
version: "MigrationDemoV2",
|
version: "MigrationDemoV2",
|
||||||
entityType: OrganismV2.self,
|
entityType: OrganismV2.self,
|
||||||
migrationChain: [
|
migrationChain: [
|
||||||
@@ -70,6 +104,7 @@ class MigrationsDemoViewController: UITableViewController {
|
|||||||
]
|
]
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
|
label: "Model V3",
|
||||||
version: "MigrationDemoV3",
|
version: "MigrationDemoV3",
|
||||||
entityType: OrganismV3.self,
|
entityType: OrganismV3.self,
|
||||||
migrationChain: ["MigrationDemo", "MigrationDemoV2", "MigrationDemoV3"]
|
migrationChain: ["MigrationDemo", "MigrationDemoV2", "MigrationDemoV3"]
|
||||||
@@ -81,6 +116,8 @@ class MigrationsDemoViewController: UITableViewController {
|
|||||||
|
|
||||||
@IBOutlet private dynamic weak var titleLabel: UILabel?
|
@IBOutlet private dynamic weak var titleLabel: UILabel?
|
||||||
@IBOutlet private dynamic weak var organismLabel: UILabel?
|
@IBOutlet private dynamic weak var organismLabel: UILabel?
|
||||||
|
@IBOutlet private dynamic weak var segmentedControl: UISegmentedControl?
|
||||||
|
@IBOutlet private dynamic weak var progressView: UIProgressView?
|
||||||
|
|
||||||
@IBAction private dynamic func mutateBarButtonTapped(sender: AnyObject?) {
|
@IBAction private dynamic func mutateBarButtonTapped(sender: AnyObject?) {
|
||||||
|
|
||||||
@@ -93,11 +130,21 @@ class MigrationsDemoViewController: UITableViewController {
|
|||||||
|
|
||||||
transaction.commit()
|
transaction.commit()
|
||||||
}
|
}
|
||||||
self.updateDisplay()
|
self.updateDisplayWithCompletion()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func selectModelVersion(model: ModelMetadata) {
|
@IBAction private dynamic func segmentedControlValueChanged(sender: AnyObject?) {
|
||||||
|
|
||||||
|
guard let index = self.segmentedControl?.selectedSegmentIndex else {
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.selectModelVersion(self.models[index], animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func selectModelVersion(model: ModelMetadata, animated: Bool) {
|
||||||
|
|
||||||
if self.organism?.entity.managedObjectClassName == "\(model.entityType)" {
|
if self.organism?.entity.managedObjectClassName == "\(model.entityType)" {
|
||||||
|
|
||||||
@@ -112,8 +159,8 @@ class MigrationsDemoViewController: UITableViewController {
|
|||||||
migrationChain: model.migrationChain
|
migrationChain: model.migrationChain
|
||||||
)
|
)
|
||||||
|
|
||||||
self.setEnabled(false)
|
self.setEnabled(false, animated: animated)
|
||||||
dataStack.addSQLiteStore(
|
let progress = try! dataStack.addSQLiteStore(
|
||||||
fileName: "MigrationDemo.sqlite",
|
fileName: "MigrationDemo.sqlite",
|
||||||
completion: { [weak self] (result) -> Void in
|
completion: { [weak self] (result) -> Void in
|
||||||
|
|
||||||
@@ -124,7 +171,7 @@ class MigrationsDemoViewController: UITableViewController {
|
|||||||
|
|
||||||
guard case .Success = result else {
|
guard case .Success = result else {
|
||||||
|
|
||||||
strongSelf.setEnabled(true)
|
strongSelf.setEnabled(true, animated: animated)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,32 +184,44 @@ class MigrationsDemoViewController: UITableViewController {
|
|||||||
|
|
||||||
dataStack.beginSynchronous { (transaction) -> Void in
|
dataStack.beginSynchronous { (transaction) -> Void in
|
||||||
|
|
||||||
let organism = transaction.create(Into(model.entityType))
|
for _ in 0 ..< 100000 {
|
||||||
(organism as! OrganismProtocol).mutate()
|
|
||||||
|
let organism = transaction.create(Into(model.entityType))
|
||||||
|
(organism as! OrganismProtocol).mutate()
|
||||||
|
}
|
||||||
|
|
||||||
transaction.commit()
|
transaction.commit()
|
||||||
}
|
}
|
||||||
strongSelf.organism = dataStack.fetchOne(From(model.entityType))!
|
strongSelf.organism = dataStack.fetchOne(From(model.entityType))!
|
||||||
}
|
}
|
||||||
|
|
||||||
strongSelf.updateDisplay()
|
strongSelf.updateDisplayWithCompletion()
|
||||||
|
|
||||||
|
let indexOfModel = strongSelf.models.map { $0.version }.indexOf(model.version)!
|
||||||
strongSelf.tableView.selectRowAtIndexPath(
|
strongSelf.tableView.selectRowAtIndexPath(
|
||||||
NSIndexPath(
|
NSIndexPath(forRow: indexOfModel, inSection: 0),
|
||||||
forRow: strongSelf.models.map { $0.version }.indexOf(model.version)!,
|
|
||||||
inSection: 0
|
|
||||||
),
|
|
||||||
animated: false,
|
animated: false,
|
||||||
scrollPosition: .None
|
scrollPosition: .None
|
||||||
)
|
)
|
||||||
strongSelf.setEnabled(true)
|
strongSelf.segmentedControl?.selectedSegmentIndex = indexOfModel
|
||||||
|
strongSelf.setEnabled(true, animated: animated)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if let progress = progress {
|
||||||
|
|
||||||
|
self.updateDisplayWithProgress(progress)
|
||||||
|
progress.setProgressHandler { [weak self] (progress) -> Void in
|
||||||
|
|
||||||
|
self?.updateDisplayWithProgress(progress)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func setEnabled(enabled: Bool) {
|
func setEnabled(enabled: Bool, animated: Bool) {
|
||||||
|
|
||||||
UIView.animateKeyframesWithDuration(
|
UIView.animateKeyframesWithDuration(
|
||||||
0.2,
|
animated ? 0.2 : 0,
|
||||||
delay: 0,
|
delay: 0,
|
||||||
options: .BeginFromCurrentState,
|
options: .BeginFromCurrentState,
|
||||||
animations: { () -> Void in
|
animations: { () -> Void in
|
||||||
@@ -182,7 +241,14 @@ class MigrationsDemoViewController: UITableViewController {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateDisplay() {
|
func updateDisplayWithProgress(progress: NSProgress) {
|
||||||
|
|
||||||
|
self.progressView?.setProgress(Float(progress.fractionCompleted), animated: true)
|
||||||
|
self.titleLabel?.text = "Migrating: \(progress.localizedDescription)"
|
||||||
|
self.organismLabel?.text = "Incremental step \(progress.localizedAdditionalDescription)"
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateDisplayWithCompletion() {
|
||||||
|
|
||||||
var lines = [String]()
|
var lines = [String]()
|
||||||
var organismType = ""
|
var organismType = ""
|
||||||
@@ -193,11 +259,12 @@ class MigrationsDemoViewController: UITableViewController {
|
|||||||
let value: AnyObject = organism.valueForKey(property.name) ?? NSNull()
|
let value: AnyObject = organism.valueForKey(property.name) ?? NSNull()
|
||||||
lines.append("\(property.name): \(value)")
|
lines.append("\(property.name): \(value)")
|
||||||
}
|
}
|
||||||
organismType = "\(objc_getClass(organism.entity.managedObjectClassName))"
|
organismType = organism.entity.managedObjectClassName
|
||||||
}
|
}
|
||||||
|
|
||||||
self.titleLabel?.text = organismType
|
self.titleLabel?.text = organismType
|
||||||
self.organismLabel?.text = "\n".join(lines)
|
self.organismLabel?.text = "\n".join(lines)
|
||||||
|
self.progressView?.progress = 0
|
||||||
self.tableView.tableHeaderView?.setNeedsLayout()
|
self.tableView.tableHeaderView?.setNeedsLayout()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user