mirror of
https://github.com/JohnEstropia/CoreStore.git
synced 2026-01-16 05:56:50 +01: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 */; };
|
||||
B5E84F391AFF85470064E85B /* NSManagedObjectContext+Querying.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E84F351AFF85470064E85B /* NSManagedObjectContext+Querying.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 */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -158,6 +160,8 @@
|
||||
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>"; };
|
||||
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 */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -383,6 +387,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B5E84F271AFF84920064E85B /* NSManagedObject+Convenience.swift */,
|
||||
B5FAD6A81B50A4B300714891 /* NSProgress+Convenience.swift */,
|
||||
);
|
||||
path = "Convenience Helpers";
|
||||
sourceTree = "<group>";
|
||||
@@ -391,13 +396,14 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B5E84F2A1AFF849C0064E85B /* AssociatedObjects.swift */,
|
||||
B51BE0691B47FC4B0069F532 /* NSManagedObjectModel+Setup.swift */,
|
||||
B5E84F2B1AFF849C0064E85B /* NotificationObserver.swift */,
|
||||
B5FAD6AB1B51285300714891 /* MigrationManager.swift */,
|
||||
B5E84F341AFF85470064E85B /* NSManagedObject+Transaction.swift */,
|
||||
B5E84F2C1AFF849C0064E85B /* NSManagedObjectContext+CoreStore.swift */,
|
||||
B5E84F351AFF85470064E85B /* NSManagedObjectContext+Querying.swift */,
|
||||
B5E84F321AFF85470064E85B /* NSManagedObjectContext+Setup.swift */,
|
||||
B5E84F331AFF85470064E85B /* NSManagedObjectContext+Transaction.swift */,
|
||||
B51BE0691B47FC4B0069F532 /* NSManagedObjectModel+Setup.swift */,
|
||||
B5E84F2D1AFF849C0064E85B /* WeakObject.swift */,
|
||||
);
|
||||
path = Internal;
|
||||
@@ -558,6 +564,7 @@
|
||||
2F291E2719C6D3CF007AF63F /* CoreStore.swift in Sources */,
|
||||
B5E84F411AFF8CCD0064E85B /* ClauseTypes.swift in Sources */,
|
||||
B5E84F0D1AFF847B0064E85B /* BaseDataTransaction+Querying.swift in Sources */,
|
||||
B5FAD6AC1B51285300714891 /* MigrationManager.swift in Sources */,
|
||||
B5E84EF61AFF846E0064E85B /* DataStack+Transaction.swift in Sources */,
|
||||
B5E84EDF1AFF84500064E85B /* DataStack.swift in Sources */,
|
||||
B5E84F231AFF84860064E85B /* ListMonitor.swift in Sources */,
|
||||
@@ -566,6 +573,7 @@
|
||||
B5E84EF51AFF846E0064E85B /* BaseDataTransaction.swift in Sources */,
|
||||
B5E84EFB1AFF846E0064E85B /* SaveResult.swift in Sources */,
|
||||
B5E84F0F1AFF847B0064E85B /* From.swift in Sources */,
|
||||
B5FAD6A91B50A4B400714891 /* NSProgress+Convenience.swift in Sources */,
|
||||
B5E84EFC1AFF846E0064E85B /* SynchronousDataTransaction.swift in Sources */,
|
||||
B5E84F281AFF84920064E85B /* NSManagedObject+Convenience.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)
|
||||
context.parentContext = rootContext
|
||||
context.shouldCascadeSavesToParent = true
|
||||
context.mergePolicy = NSRollbackMergePolicy
|
||||
context.shouldCascadeSavesToParent = false
|
||||
context.undoManager = nil
|
||||
context.setupForCoreStoreWithContextName("com.corestore.maincontext")
|
||||
context.observerForDidSaveNotification = NotificationObserver(
|
||||
|
||||
@@ -33,15 +33,65 @@ internal extension NSManagedObjectModel {
|
||||
|
||||
// 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 {
|
||||
|
||||
@@ -61,7 +111,7 @@ internal extension NSManagedObjectModel {
|
||||
}
|
||||
}
|
||||
|
||||
private(set) var modelVersions: Set<String>? {
|
||||
@nonobjc private(set) internal var modelVersions: Set<String>? {
|
||||
|
||||
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)]!
|
||||
}
|
||||
|
||||
func mergedModels() -> [NSManagedObjectModel] {
|
||||
@nonobjc internal func mergedModels() -> [NSManagedObjectModel] {
|
||||
|
||||
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 {
|
||||
|
||||
@@ -117,68 +167,32 @@ internal extension NSManagedObjectModel {
|
||||
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)")
|
||||
}
|
||||
|
||||
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 {
|
||||
for modelVersion in self.modelVersions ?? [] {
|
||||
|
||||
CoreStore.fatalError("Could not load \(typeName(NSManagedObjectModel)) metadata from path \"\(versionInfoPlistURL)\"."
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
if let versionModel = self[modelVersion] where modelHashes == versionModel.entityVersionHashesByName {
|
||||
|
||||
return versionModel
|
||||
}
|
||||
}
|
||||
|
||||
CoreStore.assert(
|
||||
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
|
||||
}
|
||||
|
||||
CoreStore.fatalError("Could not create an \(typeName(NSManagedObjectModel)) from the model at URL \"\(modelFileURL)\".")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private var modelFileURL: NSURL? {
|
||||
|
||||
get {
|
||||
|
||||
return self.modelVersionFileURL?.URLByDeletingLastPathComponent
|
||||
}
|
||||
}
|
||||
|
||||
private var modelVersionFileURL: NSURL? {
|
||||
|
||||
get {
|
||||
|
||||
@@ -46,10 +46,8 @@ public extension CoreStore {
|
||||
level: level,
|
||||
message: message,
|
||||
fileName: fileName,
|
||||
lineNumber:
|
||||
lineNumber,
|
||||
functionName:
|
||||
functionName
|
||||
lineNumber: lineNumber,
|
||||
functionName: functionName
|
||||
)
|
||||
}
|
||||
|
||||
@@ -74,14 +72,4 @@ public extension CoreStore {
|
||||
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
|
||||
*/
|
||||
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 {
|
||||
|
||||
return "<\(_stdlib_getDemangledTypeName(value))>"
|
||||
return "'\(_stdlib_getDemangledTypeName(value))'"
|
||||
}
|
||||
|
||||
internal func typeName<T>(value: T.Type) -> String {
|
||||
|
||||
return "<\(value)>"
|
||||
return "'\(value)'"
|
||||
}
|
||||
|
||||
internal func typeName(value: AnyClass) -> String {
|
||||
|
||||
return "<\(value)>"
|
||||
return "'\(value)'"
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
#if DEBUG
|
||||
let icon: String
|
||||
let levelString: String
|
||||
switch level {
|
||||
case .Trace: levelString = "Trace"
|
||||
case .Notice: levelString = "Notice"
|
||||
case .Warning: levelString = "Warning"
|
||||
case .Fatal: levelString = "Fatal"
|
||||
|
||||
case .Trace:
|
||||
icon = "🔹"
|
||||
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
|
||||
}
|
||||
|
||||
public func handleError(error error: NSError, message: String, fileName: StaticString, lineNumber: Int, functionName: StaticString) {
|
||||
|
||||
#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
|
||||
}
|
||||
|
||||
public func assert(@autoclosure condition: () -> Bool, message: String, fileName: StaticString, lineNumber: Int, functionName: StaticString) {
|
||||
|
||||
#if DEBUG
|
||||
Swift.assert(condition, message, file: fileName, line: numericCast(lineNumber))
|
||||
Swift.assert(condition, "❗ [CoreStore: Assertion Failure] \(message)", file: fileName, line: numericCast(lineNumber))
|
||||
#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 {
|
||||
|
||||
/**
|
||||
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 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()`.
|
||||
: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(
|
||||
fileName,
|
||||
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()`.
|
||||
: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]
|
||||
do {
|
||||
@@ -74,101 +259,30 @@ public extension DataStack {
|
||||
|
||||
CoreStore.handleError(
|
||||
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;
|
||||
let destinationModel = coordinator.managedObjectModel
|
||||
if destinationModel.isConfiguration(configuration, compatibleWithStoreMetadata: metadata) {
|
||||
guard let migrationSteps = self.computeMigrationFromStoreMetadata(metadata, configuration: configuration, mappingModelBundles: mappingModelBundles) else {
|
||||
|
||||
return .None
|
||||
}
|
||||
|
||||
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
|
||||
let error = NSError(coreStoreErrorCode: .MappingModelNotFound)
|
||||
CoreStore.handleError(
|
||||
metadataError,
|
||||
"Failed to load SQLite \(typeName(NSPersistentStore)) metadata at \"\(fileURL)\"."
|
||||
error,
|
||||
"Failed to find migration steps from the store at URL \"\(fileURL)\" to version model \"\(self.modelVersion)\"."
|
||||
)
|
||||
|
||||
GCDQueue.Main.async {
|
||||
|
||||
completion(MigrationResult(metadataError))
|
||||
}
|
||||
return nil
|
||||
throw error
|
||||
}
|
||||
|
||||
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(
|
||||
NSError(coreStoreErrorCode: .MappingModelNotFound),
|
||||
@@ -182,30 +296,28 @@ public extension DataStack {
|
||||
return nil
|
||||
}
|
||||
|
||||
if migrationSteps.count == 0 {
|
||||
let numberOfMigrations: Int64 = Int64(migrationSteps.count)
|
||||
if numberOfMigrations == 0 {
|
||||
|
||||
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 operations = [NSOperation]()
|
||||
var cancelled = false
|
||||
for (sourceModel, destinationModel, mappingModel, migrationType) in migrationSteps {
|
||||
|
||||
let progress = NSProgress(totalUnitCount: numberOfMigrations)
|
||||
|
||||
for (sourceModel, destinationModel, mappingModel, _) in migrationSteps {
|
||||
|
||||
switch (mergedMigrationType, migrationType) {
|
||||
|
||||
case (.None, _), (.Lightweight, .Heavyweight):
|
||||
mergedMigrationType = migrationType
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
progress.becomeCurrentWithPendingUnitCount(1)
|
||||
let childProgress = NSProgress(totalUnitCount: 100)
|
||||
|
||||
operations.append(
|
||||
NSBlockOperation { [weak self] in
|
||||
@@ -223,8 +335,10 @@ public extension DataStack {
|
||||
fileURL: fileURL,
|
||||
sourceModel: sourceModel,
|
||||
destinationModel: destinationModel,
|
||||
mappingModel: mappingModel
|
||||
mappingModel: mappingModel,
|
||||
progress: childProgress
|
||||
)
|
||||
childProgress.setProgressHandler(nil)
|
||||
}
|
||||
catch {
|
||||
|
||||
@@ -232,8 +346,16 @@ public extension DataStack {
|
||||
cancelled = true
|
||||
}
|
||||
}
|
||||
|
||||
GCDQueue.Main.async {
|
||||
|
||||
withExtendedLifetime(childProgress) { (_: NSProgress) -> Void in }
|
||||
return
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
progress.resignCurrent()
|
||||
}
|
||||
|
||||
let migrationOperation = NSBlockOperation()
|
||||
@@ -243,7 +365,9 @@ public extension DataStack {
|
||||
|
||||
GCDQueue.Main.async {
|
||||
|
||||
completion(migrationResult ?? MigrationResult(mergedMigrationType))
|
||||
completion(migrationResult ?? MigrationResult(migrationTypes))
|
||||
|
||||
withExtendedLifetime(progress) { (_: NSProgress) -> Void in }
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -252,105 +376,10 @@ public extension DataStack {
|
||||
|
||||
self.migrationQueue.addOperations(operations, waitUntilFinished: false)
|
||||
|
||||
return mergedMigrationType
|
||||
return progress
|
||||
}
|
||||
|
||||
/**
|
||||
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)]? {
|
||||
private func computeMigrationFromStoreMetadata(metadata: [String: AnyObject], configuration: String? = nil, mappingModelBundles: [NSBundle]? = nil) -> [(sourceModel: NSManagedObjectModel, destinationModel: NSManagedObjectModel, mappingModel: NSMappingModel, migrationType: MigrationType)]? {
|
||||
|
||||
let model = self.model
|
||||
if model.isConfiguration(configuration, compatibleWithStoreMetadata: metadata) {
|
||||
@@ -358,42 +387,16 @@ public extension DataStack {
|
||||
return []
|
||||
}
|
||||
|
||||
let metadataModel = NSManagedObjectModel(byMergingModels: model.mergedModels(), forStoreMetadata: metadata)!
|
||||
if let bypassModel = NSMappingModel(
|
||||
fromBundles: sourceBundles,
|
||||
forSourceModel: metadataModel,
|
||||
destinationModel: model) {
|
||||
guard let initialModel = model[metadata],
|
||||
var currentVersion = initialModel.currentModelVersion else {
|
||||
|
||||
return [
|
||||
(
|
||||
sourceModel: metadataModel,
|
||||
destinationModel: model,
|
||||
mappingModel: bypassModel,
|
||||
migrationType: .Heavyweight
|
||||
)
|
||||
]
|
||||
return nil
|
||||
}
|
||||
|
||||
var initialModel: NSManagedObjectModel?
|
||||
if let modelHashes = metadata[NSStoreModelVersionHashesKey] as? [String : NSData],
|
||||
let modelVersions = model.modelVersions {
|
||||
|
||||
for modelVersion in modelVersions {
|
||||
|
||||
if let versionModel = model[modelVersion] where modelHashes == versionModel.entityVersionHashesByName {
|
||||
|
||||
initialModel = versionModel
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
let migrationChain: MigrationChain = self.migrationChain.empty
|
||||
? [currentVersion: model.currentModelVersion!]
|
||||
: self.migrationChain
|
||||
|
||||
guard var currentVersion = initialModel?.currentModelVersion else {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
let migrationChain = self.migrationChain
|
||||
var migrationSteps = [(sourceModel: NSManagedObjectModel, destinationModel: NSManagedObjectModel, mappingModel: NSMappingModel, migrationType: MigrationType)]()
|
||||
|
||||
while let nextVersion = migrationChain.nextVersionFrom(currentVersion),
|
||||
@@ -401,7 +404,7 @@ public extension DataStack {
|
||||
let destinationModel = model[nextVersion] {
|
||||
|
||||
if let mappingModel = NSMappingModel(
|
||||
fromBundles: sourceBundles,
|
||||
fromBundles: mappingModelBundles,
|
||||
forSourceModel: sourceModel,
|
||||
destinationModel: destinationModel) {
|
||||
|
||||
@@ -410,7 +413,10 @@ public extension DataStack {
|
||||
sourceModel: sourceModel,
|
||||
destinationModel: destinationModel,
|
||||
mappingModel: mappingModel,
|
||||
migrationType: .Heavyweight
|
||||
migrationType: .Heavyweight(
|
||||
sourceVersion: currentVersion,
|
||||
destinationVersion: nextVersion
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -428,7 +434,10 @@ public extension DataStack {
|
||||
sourceModel: sourceModel,
|
||||
destinationModel: destinationModel,
|
||||
mappingModel: mappingModel,
|
||||
migrationType: .Lightweight
|
||||
migrationType: .Lightweight(
|
||||
sourceVersion: currentVersion,
|
||||
destinationVersion: nextVersion
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -448,7 +457,7 @@ public extension DataStack {
|
||||
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 {
|
||||
|
||||
@@ -462,25 +471,10 @@ public extension DataStack {
|
||||
try! journalUpdatingCoordinator.removePersistentStore(store)
|
||||
}
|
||||
|
||||
let migrationManager = NSMigrationManager(
|
||||
let migrationManager = MigrationManager(
|
||||
sourceModel: sourceModel,
|
||||
destinationModel: destinationModel
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
destinationModel: destinationModel,
|
||||
progress: progress
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
do {
|
||||
|
||||
try migrationManager.migrateStoreFromURL(
|
||||
@@ -507,25 +502,22 @@ public extension DataStack {
|
||||
}
|
||||
catch {
|
||||
|
||||
timer.suspend()
|
||||
|
||||
do {
|
||||
|
||||
try fileManager.removeItemAtURL(temporaryDirectoryURL)
|
||||
}
|
||||
catch _ { }
|
||||
|
||||
let migrationError = error as NSError
|
||||
let sourceVersion = migrationManager.sourceModel.currentModelVersion ?? "???"
|
||||
let destinationVersion = migrationManager.destinationModel.currentModelVersion ?? "???"
|
||||
CoreStore.handleError(
|
||||
migrationError,
|
||||
"Failed to migrate from version model \"\(migrationManager.sourceModel)\" to version model \"\(migrationManager.destinationModel)\"."
|
||||
error as NSError,
|
||||
"Failed to migrate from version model \"\(sourceVersion)\" to version model \"\(destinationVersion)\"."
|
||||
)
|
||||
|
||||
throw error
|
||||
}
|
||||
|
||||
timer.suspend()
|
||||
|
||||
do {
|
||||
|
||||
try fileManager.replaceItemAtURL(
|
||||
@@ -536,6 +528,8 @@ public extension DataStack {
|
||||
resultingItemURL: nil
|
||||
)
|
||||
|
||||
progress.completedUnitCount = progress.totalUnitCount
|
||||
|
||||
do {
|
||||
|
||||
try fileManager.removeItemAtPath(fileURL.path! + "-shm")
|
||||
@@ -550,10 +544,11 @@ public extension DataStack {
|
||||
}
|
||||
catch _ { }
|
||||
|
||||
let replaceError = error as NSError
|
||||
let sourceVersion = migrationManager.sourceModel.currentModelVersion ?? "???"
|
||||
let destinationVersion = migrationManager.destinationModel.currentModelVersion ?? "???"
|
||||
CoreStore.handleError(
|
||||
replaceError,
|
||||
"Failed to save store after migrating from version model \"\(migrationManager.sourceModel)\" to version model \"\(migrationManager.destinationModel)\"."
|
||||
error as NSError,
|
||||
"Failed to save store after migrating from version model \"\(sourceVersion)\" to version model \"\(destinationVersion)\"."
|
||||
)
|
||||
|
||||
throw error
|
||||
|
||||
@@ -137,6 +137,11 @@ public struct MigrationChain: NilLiteralConvertible, StringLiteralConvertible, D
|
||||
internal let leafVersions: Set<String>
|
||||
internal let valid: Bool
|
||||
|
||||
internal var empty: Bool {
|
||||
|
||||
return self.versionTree.count <= 0
|
||||
}
|
||||
|
||||
internal func contains(version: String) -> Bool {
|
||||
|
||||
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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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
|
||||
@@ -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.
|
||||
*/
|
||||
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.
|
||||
@@ -113,9 +149,9 @@ public enum MigrationResult {
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
internal init(_ migrationType: MigrationType) {
|
||||
internal init(_ migrationTypes: [MigrationType]) {
|
||||
|
||||
self = .Success(migrationType)
|
||||
self = .Success(migrationTypes)
|
||||
}
|
||||
|
||||
internal init(_ error: NSError) {
|
||||
|
||||
@@ -70,10 +70,10 @@ public /*abstract*/ class BaseDataTransaction {
|
||||
return object
|
||||
|
||||
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:
|
||||
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 {
|
||||
@@ -88,11 +88,11 @@ public /*abstract*/ class BaseDataTransaction {
|
||||
default:
|
||||
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 {
|
||||
|
||||
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`.
|
||||
|
||||
- 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 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
|
||||
}
|
||||
|
||||
/**
|
||||
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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
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) {
|
||||
|
||||
|
||||
@@ -136,31 +136,54 @@
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<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">
|
||||
<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"/>
|
||||
<color key="textColor" red="0.92549019610000005" green="0.94117647059999998" blue="0.94509803920000002" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</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">
|
||||
<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"/>
|
||||
<color key="textColor" red="0.92549019610000005" green="0.94117647059999998" blue="0.94509803920000002" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</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>
|
||||
<color key="backgroundColor" red="0.20392156859999999" green="0.28627450980000002" blue="0.36862745099999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<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 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="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="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"/>
|
||||
</constraints>
|
||||
<variation key="default">
|
||||
<mask key="constraints">
|
||||
<exclude reference="DC3-u7-f9M"/>
|
||||
<exclude reference="tYt-wb-Lk8"/>
|
||||
</mask>
|
||||
</variation>
|
||||
</view>
|
||||
@@ -206,6 +229,8 @@
|
||||
<simulatedToolbarMetrics key="simulatedBottomBarMetrics"/>
|
||||
<connections>
|
||||
<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"/>
|
||||
</connections>
|
||||
</tableViewController>
|
||||
|
||||
@@ -19,7 +19,39 @@ class MigrationsDemoViewController: UITableViewController {
|
||||
override func 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) {
|
||||
|
||||
self.selectModelVersion(self.models[indexPath.row])
|
||||
self.selectModelVersion(self.models[indexPath.row], animated: true)
|
||||
}
|
||||
|
||||
|
||||
// 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] = [
|
||||
(
|
||||
label: "Model V1",
|
||||
version: "MigrationDemo",
|
||||
entityType: OrganismV1.self,
|
||||
migrationChain: ["MigrationDemoV3", "MigrationDemoV2", "MigrationDemo"]
|
||||
),
|
||||
(
|
||||
label: "Model V2",
|
||||
version: "MigrationDemoV2",
|
||||
entityType: OrganismV2.self,
|
||||
migrationChain: [
|
||||
@@ -70,6 +104,7 @@ class MigrationsDemoViewController: UITableViewController {
|
||||
]
|
||||
),
|
||||
(
|
||||
label: "Model V3",
|
||||
version: "MigrationDemoV3",
|
||||
entityType: OrganismV3.self,
|
||||
migrationChain: ["MigrationDemo", "MigrationDemoV2", "MigrationDemoV3"]
|
||||
@@ -81,6 +116,8 @@ class MigrationsDemoViewController: UITableViewController {
|
||||
|
||||
@IBOutlet private dynamic weak var titleLabel: 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?) {
|
||||
|
||||
@@ -93,11 +130,21 @@ class MigrationsDemoViewController: UITableViewController {
|
||||
|
||||
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)" {
|
||||
|
||||
@@ -112,8 +159,8 @@ class MigrationsDemoViewController: UITableViewController {
|
||||
migrationChain: model.migrationChain
|
||||
)
|
||||
|
||||
self.setEnabled(false)
|
||||
dataStack.addSQLiteStore(
|
||||
self.setEnabled(false, animated: animated)
|
||||
let progress = try! dataStack.addSQLiteStore(
|
||||
fileName: "MigrationDemo.sqlite",
|
||||
completion: { [weak self] (result) -> Void in
|
||||
|
||||
@@ -124,7 +171,7 @@ class MigrationsDemoViewController: UITableViewController {
|
||||
|
||||
guard case .Success = result else {
|
||||
|
||||
strongSelf.setEnabled(true)
|
||||
strongSelf.setEnabled(true, animated: animated)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -137,32 +184,44 @@ class MigrationsDemoViewController: UITableViewController {
|
||||
|
||||
dataStack.beginSynchronous { (transaction) -> Void in
|
||||
|
||||
let organism = transaction.create(Into(model.entityType))
|
||||
(organism as! OrganismProtocol).mutate()
|
||||
for _ in 0 ..< 100000 {
|
||||
|
||||
let organism = transaction.create(Into(model.entityType))
|
||||
(organism as! OrganismProtocol).mutate()
|
||||
}
|
||||
|
||||
transaction.commit()
|
||||
}
|
||||
strongSelf.organism = dataStack.fetchOne(From(model.entityType))!
|
||||
}
|
||||
|
||||
strongSelf.updateDisplay()
|
||||
strongSelf.updateDisplayWithCompletion()
|
||||
|
||||
let indexOfModel = strongSelf.models.map { $0.version }.indexOf(model.version)!
|
||||
strongSelf.tableView.selectRowAtIndexPath(
|
||||
NSIndexPath(
|
||||
forRow: strongSelf.models.map { $0.version }.indexOf(model.version)!,
|
||||
inSection: 0
|
||||
),
|
||||
NSIndexPath(forRow: indexOfModel, inSection: 0),
|
||||
animated: false,
|
||||
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(
|
||||
0.2,
|
||||
animated ? 0.2 : 0,
|
||||
delay: 0,
|
||||
options: .BeginFromCurrentState,
|
||||
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 organismType = ""
|
||||
@@ -193,11 +259,12 @@ class MigrationsDemoViewController: UITableViewController {
|
||||
let value: AnyObject = organism.valueForKey(property.name) ?? NSNull()
|
||||
lines.append("\(property.name): \(value)")
|
||||
}
|
||||
organismType = "\(objc_getClass(organism.entity.managedObjectClassName))"
|
||||
organismType = organism.entity.managedObjectClassName
|
||||
}
|
||||
|
||||
self.titleLabel?.text = organismType
|
||||
self.organismLabel?.text = "\n".join(lines)
|
||||
self.progressView?.progress = 0
|
||||
self.tableView.tableHeaderView?.setNeedsLayout()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user