Stores SaveState model objects with Core Data, and displays the available save states in SaveStatesViewController. Currently, tapping + button creates a new save state, while tapping a collection view cell loads the save state, regardless of whether Save State or Load State was selected from the pause menu.
344 lines
12 KiB
Swift
344 lines
12 KiB
Swift
//
|
|
// DatabaseManager.swift
|
|
// Delta
|
|
//
|
|
// Created by Riley Testut on 10/4/15.
|
|
// Copyright © 2015 Riley Testut. All rights reserved.
|
|
//
|
|
|
|
import CoreData
|
|
|
|
// Workspace
|
|
import Roxas
|
|
import DeltaCore
|
|
|
|
// Pods
|
|
import FileMD5Hash
|
|
|
|
class DatabaseManager
|
|
{
|
|
static let sharedManager = DatabaseManager()
|
|
|
|
let managedObjectContext: NSManagedObjectContext
|
|
|
|
private let privateManagedObjectContext: NSManagedObjectContext
|
|
private let validationManagedObjectContext: NSManagedObjectContext
|
|
|
|
// MARK: - Initialization -
|
|
/// Initialization
|
|
|
|
private init()
|
|
{
|
|
let modelURL = NSBundle.mainBundle().URLForResource("Model", withExtension: "momd")
|
|
let managedObjectModel = NSManagedObjectModel(contentsOfURL: modelURL!)
|
|
let persistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: managedObjectModel!)
|
|
|
|
self.privateManagedObjectContext = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
|
|
self.privateManagedObjectContext.persistentStoreCoordinator = persistentStoreCoordinator
|
|
self.privateManagedObjectContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
|
|
|
|
self.managedObjectContext = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType)
|
|
self.managedObjectContext.parentContext = self.privateManagedObjectContext
|
|
self.managedObjectContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
|
|
|
|
self.validationManagedObjectContext = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
|
|
self.validationManagedObjectContext.parentContext = self.managedObjectContext
|
|
self.validationManagedObjectContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
|
|
|
|
NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("managedObjectContextDidSave:"), name: NSManagedObjectContextDidSaveNotification, object: nil)
|
|
|
|
}
|
|
|
|
func startWithCompletion(completionBlock: ((performingMigration: Bool) -> Void)?)
|
|
{
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) {
|
|
|
|
let storeURL = DatabaseManager.databaseDirectoryURL.URLByAppendingPathComponent("Delta.sqlite")
|
|
|
|
let options = [NSMigratePersistentStoresAutomaticallyOption: true, NSInferMappingModelAutomaticallyOption: true]
|
|
|
|
var performingMigration = false
|
|
|
|
if let sourceMetadata = try? NSPersistentStoreCoordinator.metadataForPersistentStoreOfType(NSSQLiteStoreType, URL: storeURL, options: options),
|
|
managedObjectModel = self.privateManagedObjectContext.persistentStoreCoordinator?.managedObjectModel
|
|
{
|
|
performingMigration = !managedObjectModel.isConfiguration(nil, compatibleWithStoreMetadata: sourceMetadata)
|
|
}
|
|
|
|
do
|
|
{
|
|
try self.privateManagedObjectContext.persistentStoreCoordinator?.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: storeURL, options: options)
|
|
}
|
|
catch let error as NSError
|
|
{
|
|
if error.code == NSMigrationMissingSourceModelError
|
|
{
|
|
print("Migration failed. Try deleting \(storeURL)")
|
|
}
|
|
else
|
|
{
|
|
print(error)
|
|
}
|
|
|
|
abort()
|
|
}
|
|
|
|
if let completionBlock = completionBlock
|
|
{
|
|
completionBlock(performingMigration: performingMigration)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Importing -
|
|
/// Importing
|
|
|
|
func importGamesAtURLs(URLs: [NSURL], withCompletion completion: ([String] -> Void)?)
|
|
{
|
|
let managedObjectContext = self.backgroundManagedObjectContext()
|
|
managedObjectContext.performBlock() {
|
|
|
|
var identifiers: [String] = []
|
|
|
|
for URL in URLs
|
|
{
|
|
let identifier = FileHash.sha1HashOfFileAtPath(URL.path) as String
|
|
|
|
var filename = identifier
|
|
if let pathExtension = URL.pathExtension
|
|
{
|
|
filename += "." + pathExtension
|
|
}
|
|
|
|
let game = Game.insertIntoManagedObjectContext(managedObjectContext)
|
|
game.name = URL.URLByDeletingPathExtension?.lastPathComponent ?? NSLocalizedString("Game", comment: "")
|
|
game.identifier = identifier
|
|
game.filename = filename
|
|
|
|
if let pathExtension = URL.pathExtension
|
|
{
|
|
let gameCollection = GameCollection.gameSystemCollectionForPathExtension(pathExtension, inManagedObjectContext: managedObjectContext)
|
|
game.typeIdentifier = gameCollection.identifier
|
|
game.gameCollections.insert(gameCollection)
|
|
}
|
|
else
|
|
{
|
|
game.typeIdentifier = kUTTypeDeltaGame as String
|
|
}
|
|
|
|
do
|
|
{
|
|
let destinationURL = DatabaseManager.gamesDirectoryURL.URLByAppendingPathComponent(game.identifier + "." + game.preferredFileExtension)
|
|
|
|
if let path = destinationURL.path
|
|
{
|
|
if NSFileManager.defaultManager().fileExistsAtPath(path)
|
|
{
|
|
try NSFileManager.defaultManager().removeItemAtURL(URL)
|
|
}
|
|
else
|
|
{
|
|
try NSFileManager.defaultManager().moveItemAtURL(URL, toURL: destinationURL)
|
|
}
|
|
}
|
|
|
|
identifiers.append(game.identifier)
|
|
}
|
|
catch
|
|
{
|
|
game.managedObjectContext?.deleteObject(game)
|
|
}
|
|
|
|
}
|
|
|
|
do
|
|
{
|
|
try managedObjectContext.save()
|
|
}
|
|
catch let error as NSError
|
|
{
|
|
print("Failed to save import context:", error)
|
|
|
|
identifiers.removeAll()
|
|
}
|
|
|
|
if let completion = completion
|
|
{
|
|
completion(identifiers)
|
|
}
|
|
}
|
|
|
|
|
|
}
|
|
|
|
// MARK: - Background Contexts -
|
|
/// Background Contexts
|
|
|
|
func backgroundManagedObjectContext() -> NSManagedObjectContext
|
|
{
|
|
let managedObjectContext = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
|
|
managedObjectContext.parentContext = self.validationManagedObjectContext
|
|
managedObjectContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
|
|
|
|
return managedObjectContext
|
|
}
|
|
}
|
|
|
|
extension DatabaseManager
|
|
{
|
|
class var databaseDirectoryURL: NSURL
|
|
{
|
|
let documentsDirectoryURL: NSURL
|
|
|
|
if UIDevice.currentDevice().userInterfaceIdiom == .TV
|
|
{
|
|
documentsDirectoryURL = NSFileManager.defaultManager().URLsForDirectory(NSSearchPathDirectory.CachesDirectory, inDomains: NSSearchPathDomainMask.UserDomainMask).first!
|
|
}
|
|
else
|
|
{
|
|
documentsDirectoryURL = NSFileManager.defaultManager().URLsForDirectory(NSSearchPathDirectory.DocumentDirectory, inDomains: NSSearchPathDomainMask.UserDomainMask).first!
|
|
}
|
|
|
|
let databaseDirectoryURL = documentsDirectoryURL.URLByAppendingPathComponent("Database")
|
|
self.createDirectoryAtURLIfNeeded(databaseDirectoryURL)
|
|
|
|
return databaseDirectoryURL
|
|
}
|
|
|
|
class var gamesDirectoryURL: NSURL
|
|
{
|
|
let gamesDirectoryURL = DatabaseManager.databaseDirectoryURL.URLByAppendingPathComponent("Games")
|
|
self.createDirectoryAtURLIfNeeded(gamesDirectoryURL)
|
|
|
|
return gamesDirectoryURL
|
|
}
|
|
|
|
class var saveStatesDirectoryURL: NSURL
|
|
{
|
|
let saveStatesDirectoryURL = DatabaseManager.databaseDirectoryURL.URLByAppendingPathComponent("Save States")
|
|
self.createDirectoryAtURLIfNeeded(saveStatesDirectoryURL)
|
|
|
|
return saveStatesDirectoryURL
|
|
}
|
|
|
|
class func saveStatesDirectoryURLForGame(game: Game) -> NSURL
|
|
{
|
|
let gameDirectoryURL = DatabaseManager.saveStatesDirectoryURL.URLByAppendingPathComponent(game.identifier)
|
|
self.createDirectoryAtURLIfNeeded(gameDirectoryURL)
|
|
|
|
return gameDirectoryURL
|
|
}
|
|
}
|
|
|
|
private extension DatabaseManager
|
|
{
|
|
// MARK: - Saving -
|
|
|
|
func save()
|
|
{
|
|
let backgroundTaskIdentifier = RSTBeginBackgroundTask("Save Database Task")
|
|
|
|
self.validationManagedObjectContext.performBlockAndWait {
|
|
|
|
do
|
|
{
|
|
try self.validationManagedObjectContext.save()
|
|
}
|
|
catch let error as NSError
|
|
{
|
|
print("Failed to save validation context:", error)
|
|
}
|
|
|
|
|
|
// Update main managed object context
|
|
self.managedObjectContext.performBlockAndWait() {
|
|
|
|
do
|
|
{
|
|
try self.managedObjectContext.save()
|
|
}
|
|
catch let error as NSError
|
|
{
|
|
print("Failed to save main context:", error)
|
|
}
|
|
|
|
|
|
// Save to disk
|
|
self.privateManagedObjectContext.performBlock() {
|
|
|
|
do
|
|
{
|
|
try self.privateManagedObjectContext.save()
|
|
}
|
|
catch let error as NSError
|
|
{
|
|
print("Failed to save private context to disk:", error)
|
|
}
|
|
|
|
RSTEndBackgroundTask(backgroundTaskIdentifier)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
// MARK: - Validation -
|
|
|
|
func validateManagedObjectSaveWithUserInfo(userInfo: [NSObject : AnyObject])
|
|
{
|
|
// Remove deleted games from disk
|
|
if let deletedObjects = userInfo[NSDeletedObjectsKey] as? Set<NSManagedObject>
|
|
{
|
|
let games = deletedObjects.filter({ $0 is Game }).map({ self.validationManagedObjectContext.objectWithID($0.objectID) as! Game })
|
|
|
|
for game in games
|
|
{
|
|
do
|
|
{
|
|
try NSFileManager.defaultManager().removeItemAtURL(game.fileURL)
|
|
}
|
|
catch let error as NSError
|
|
{
|
|
print(error)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove empty collections
|
|
let collections = GameCollection.instancesWithPredicate(NSPredicate(format: "%K.@count == 0", GameCollectionAttributes.games.rawValue), inManagedObjectContext: self.validationManagedObjectContext, type: GameCollection.self)
|
|
|
|
for collection in collections
|
|
{
|
|
self.validationManagedObjectContext.deleteObject(collection)
|
|
}
|
|
}
|
|
|
|
// MARK: - Notifications -
|
|
|
|
dynamic func managedObjectContextDidSave(notification: NSNotification)
|
|
{
|
|
guard let managedObjectContext = notification.object as? NSManagedObjectContext where managedObjectContext.parentContext == self.validationManagedObjectContext else { return }
|
|
|
|
self.validationManagedObjectContext.performBlockAndWait {
|
|
self.validateManagedObjectSaveWithUserInfo(notification.userInfo ?? [:])
|
|
self.save()
|
|
}
|
|
}
|
|
|
|
// MARK: - File Management -
|
|
|
|
class func createDirectoryAtURLIfNeeded(URL: NSURL)
|
|
{
|
|
do
|
|
{
|
|
try NSFileManager.defaultManager().createDirectoryAtURL(URL, withIntermediateDirectories: true, attributes: nil)
|
|
}
|
|
catch
|
|
{
|
|
print(error)
|
|
}
|
|
}
|
|
}
|