diff --git a/Common/Database/DatabaseManager.swift b/Common/Database/DatabaseManager.swift index 3bfc2f9..14a18fb 100644 --- a/Common/Database/DatabaseManager.swift +++ b/Common/Database/DatabaseManager.swift @@ -21,50 +21,6 @@ class DatabaseManager let managedObjectContext: NSManagedObjectContext - 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") - - - do - { - try NSFileManager.defaultManager().createDirectoryAtURL(databaseDirectoryURL, withIntermediateDirectories: true, attributes: nil) - } - catch - { - print(error) - } - - return databaseDirectoryURL - } - - class var gamesDirectoryURL: NSURL - { - let gamesDirectoryURL = DatabaseManager.databaseDirectoryURL.URLByAppendingPathComponent("Games") - - do - { - try NSFileManager.defaultManager().createDirectoryAtURL(gamesDirectoryURL, withIntermediateDirectories: true, attributes: nil) - } - catch - { - print(error) - } - - return gamesDirectoryURL - } - private let privateManagedObjectContext: NSManagedObjectContext private let validationManagedObjectContext: NSManagedObjectContext @@ -228,6 +184,52 @@ class DatabaseManager } } +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 - @@ -324,4 +326,18 @@ private extension DatabaseManager self.save() } } + + // MARK: - File Management - + + class func createDirectoryAtURLIfNeeded(URL: NSURL) + { + do + { + try NSFileManager.defaultManager().createDirectoryAtURL(URL, withIntermediateDirectories: true, attributes: nil) + } + catch + { + print(error) + } + } } diff --git a/Common/Database/Model/GameCollection+CoreDataProperties.swift b/Common/Database/Model/GameCollection+CoreDataProperties.swift index 2a51c99..b27e39a 100644 --- a/Common/Database/Model/GameCollection+CoreDataProperties.swift +++ b/Common/Database/Model/GameCollection+CoreDataProperties.swift @@ -20,8 +20,8 @@ enum GameCollectionAttributes: String case games } -extension GameCollection { - +extension GameCollection +{ @NSManaged var identifier: String @NSManaged var index: Int16 diff --git a/Common/Database/Model/Model.xcdatamodeld/Model.xcdatamodel/contents b/Common/Database/Model/Model.xcdatamodeld/Model.xcdatamodel/contents index 4a05fa0..64dc8e4 100644 --- a/Common/Database/Model/Model.xcdatamodeld/Model.xcdatamodel/contents +++ b/Common/Database/Model/Model.xcdatamodeld/Model.xcdatamodel/contents @@ -1,12 +1,12 @@ - + - + @@ -15,6 +15,7 @@ + @@ -31,8 +32,26 @@ + + + + + + + + + + + + + + + + + - + + \ No newline at end of file diff --git a/Common/Database/Model/SaveState+CoreDataProperties.swift b/Common/Database/Model/SaveState+CoreDataProperties.swift new file mode 100644 index 0000000..633ceef --- /dev/null +++ b/Common/Database/Model/SaveState+CoreDataProperties.swift @@ -0,0 +1,37 @@ +// +// SaveState+CoreDataProperties.swift +// Delta +// +// Created by Riley Testut on 1/31/16. +// Copyright © 2016 Riley Testut. All rights reserved. +// +// Choose "Create NSManagedObject Subclass…" from the Core Data editor menu +// to delete and recreate this implementation file for your updated model. +// + +import Foundation +import CoreData + +enum SaveStateAttributes: String +{ + case filename + case identifier + case name + case creationDate + case modifiedDate + + case game +} + +extension SaveState +{ + @NSManaged var filename: String + @NSManaged var identifier: String + @NSManaged var name: String? + @NSManaged var creationDate: NSDate + @NSManaged var modifiedDate: NSDate + + // Must be optional relationship to satisfy weird Core Data requirement + // https://forums.developer.apple.com/thread/20535 + @NSManaged var game: Game! +} diff --git a/Common/Database/Model/SaveState.swift b/Common/Database/Model/SaveState.swift new file mode 100644 index 0000000..84c3d00 --- /dev/null +++ b/Common/Database/Model/SaveState.swift @@ -0,0 +1,21 @@ +// +// SaveState.swift +// Delta +// +// Created by Riley Testut on 1/31/16. +// Copyright © 2016 Riley Testut. All rights reserved. +// + +import Foundation +import CoreData + +import DeltaCore + +@objc(SaveState) +class SaveState: NSManagedObject, SaveStateType +{ + var fileURL: NSURL { + let fileURL = DatabaseManager.saveStatesDirectoryURLForGame(self.game).URLByAppendingPathComponent(self.filename) + return fileURL + } +} diff --git a/Common/Extensions/NSManagedObjectContext+Conveniences.swift b/Common/Extensions/NSManagedObjectContext+Conveniences.swift new file mode 100644 index 0000000..7f0f63a --- /dev/null +++ b/Common/Extensions/NSManagedObjectContext+Conveniences.swift @@ -0,0 +1,26 @@ +// +// NSManagedObjectContext+Conveniences.swift +// Delta +// +// Created by Riley Testut on 2/8/16. +// Copyright © 2016 Riley Testut. All rights reserved. +// + +import CoreData + +extension NSManagedObjectContext +{ + // MARK: - Saving - + + func saveWithErrorLogging() + { + do + { + try self.save() + } + catch let error as NSError + { + print("Error saving NSManagedObjectContext: ", error, error.userInfo) + } + } +} diff --git a/Cores/DeltaCore b/Cores/DeltaCore index d5b6ac7..23ce4c9 160000 --- a/Cores/DeltaCore +++ b/Cores/DeltaCore @@ -1 +1 @@ -Subproject commit d5b6ac78401dc1c9bab50bbf636f42b1c0e9fe7c +Subproject commit 23ce4c9b10760ad88d5c4c6703e88f6868a400df diff --git a/Cores/SNESDeltaCore b/Cores/SNESDeltaCore index f4e2781..6d9dbe9 160000 --- a/Cores/SNESDeltaCore +++ b/Cores/SNESDeltaCore @@ -1 +1 @@ -Subproject commit f4e27814258993234883fbca040ea66dc1d9609e +Subproject commit 6d9dbe986cdacd0cf2602035608188bcdf2c178a diff --git a/Delta.xcodeproj/project.pbxproj b/Delta.xcodeproj/project.pbxproj index 0e3850f..420ddec 100644 --- a/Delta.xcodeproj/project.pbxproj +++ b/Delta.xcodeproj/project.pbxproj @@ -10,6 +10,12 @@ AF0535CD7331785FA15E0864 /* Pods.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 22506DA00971C4300AF90A35 /* Pods.framework */; }; BF090CF41B490D8300DCAB45 /* UIDevice+Vibration.m in Sources */ = {isa = PBXBuildFile; fileRef = BF090CF31B490D8300DCAB45 /* UIDevice+Vibration.m */; }; BF107EC41BF413F000E0C32C /* GamesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF107EC31BF413F000E0C32C /* GamesViewController.swift */; }; + BF172AEB1C68986300C26774 /* NSManagedObjectContext+Conveniences.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF172AEA1C68986300C26774 /* NSManagedObjectContext+Conveniences.swift */; }; + BF172AEC1C68986300C26774 /* NSManagedObjectContext+Conveniences.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF172AEA1C68986300C26774 /* NSManagedObjectContext+Conveniences.swift */; }; + BF1FB1841C5EE643007E2494 /* SaveState+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1FB1821C5EE643007E2494 /* SaveState+CoreDataProperties.swift */; }; + BF1FB1851C5EE643007E2494 /* SaveState+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1FB1821C5EE643007E2494 /* SaveState+CoreDataProperties.swift */; }; + BF1FB1861C5EE643007E2494 /* SaveState.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1FB1831C5EE643007E2494 /* SaveState.swift */; }; + BF1FB1871C5EE643007E2494 /* SaveState.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1FB1831C5EE643007E2494 /* SaveState.swift */; }; BF27CC8B1BC9FE4D00A20D89 /* Pods.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF27CC8A1BC9FE4D00A20D89 /* Pods.framework */; }; BF27CC8C1BC9FE5300A20D89 /* Pods.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 22506DA00971C4300AF90A35 /* Pods.framework */; }; BF27CC8D1BC9FE5300A20D89 /* Pods.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 22506DA00971C4300AF90A35 /* Pods.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -127,6 +133,9 @@ BF090CF21B490D8300DCAB45 /* UIDevice+Vibration.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIDevice+Vibration.h"; sourceTree = ""; }; BF090CF31B490D8300DCAB45 /* UIDevice+Vibration.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIDevice+Vibration.m"; sourceTree = ""; }; BF107EC31BF413F000E0C32C /* GamesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GamesViewController.swift; sourceTree = ""; }; + BF172AEA1C68986300C26774 /* NSManagedObjectContext+Conveniences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext+Conveniences.swift"; sourceTree = ""; }; + BF1FB1821C5EE643007E2494 /* SaveState+CoreDataProperties.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SaveState+CoreDataProperties.swift"; sourceTree = ""; }; + BF1FB1831C5EE643007E2494 /* SaveState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SaveState.swift; sourceTree = ""; }; BF27CC861BC9E3C600A20D89 /* Delta.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = Delta.entitlements; sourceTree = ""; }; BF27CC8A1BC9FE4D00A20D89 /* Pods.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Pods.framework; path = "Pods/../build/Debug-appletvos/Pods.framework"; sourceTree = ""; }; BF27CC901BCB156200A20D89 /* EmulationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmulationViewController.swift; sourceTree = ""; }; @@ -273,6 +282,8 @@ BFDE39381BC0CEDF003F72E8 /* Game+CoreDataProperties.swift */, BFC273171BE6152200D22B05 /* GameCollection.swift */, BFC273161BE6152200D22B05 /* GameCollection+CoreDataProperties.swift */, + BF1FB1831C5EE643007E2494 /* SaveState.swift */, + BF1FB1821C5EE643007E2494 /* SaveState+CoreDataProperties.swift */, ); path = Model; sourceTree = ""; @@ -302,6 +313,7 @@ isa = PBXGroup; children = ( BF762EAA1BC1B076002C8866 /* NSManagedObject+Conveniences.swift */, + BF172AEA1C68986300C26774 /* NSManagedObjectContext+Conveniences.swift */, ); path = Extensions; sourceTree = ""; @@ -673,7 +685,9 @@ BFC273191BE6152200D22B05 /* GameCollection+CoreDataProperties.swift in Sources */, BF6BB2411BB73FE800CCF94A /* GameSelectionViewController.swift in Sources */, BF27CC8F1BCA010200A20D89 /* GamePickerController.swift in Sources */, + BF172AEC1C68986300C26774 /* NSManagedObjectContext+Conveniences.swift in Sources */, BF7AE81F1C2E984300B1B5BC /* GridCollectionViewCell.swift in Sources */, + BF1FB1851C5EE643007E2494 /* SaveState+CoreDataProperties.swift in Sources */, BF7AE8251C2E984300B1B5BC /* GridCollectionViewLayout.swift in Sources */, BFDE393D1BC0CEDF003F72E8 /* Game.swift in Sources */, BFC2731B1BE6152200D22B05 /* GameCollection.swift in Sources */, @@ -681,6 +695,7 @@ BFDE393B1BC0CEDF003F72E8 /* Game+CoreDataProperties.swift in Sources */, BF27CC911BCB156200A20D89 /* EmulationViewController.swift in Sources */, BFB141191BE46934004FBF46 /* GameCollectionViewDataSource.swift in Sources */, + BF1FB1871C5EE643007E2494 /* SaveState.swift in Sources */, BFF1E5651BE04CAF000E9EF6 /* BoxArtImageView.swift in Sources */, BF353FFA1C5D870B00C1184C /* PauseItem.swift in Sources */, BF3540061C5DA70400C1184C /* SaveStatesViewController.swift in Sources */, @@ -699,7 +714,9 @@ BF3540081C5DAFAD00C1184C /* PauseTransitionCoordinator.swift in Sources */, BFAA1FED1B8AA4FA00495943 /* Settings.swift in Sources */, BF7AE8241C2E984300B1B5BC /* GridCollectionViewLayout.swift in Sources */, + BF1FB1861C5EE643007E2494 /* SaveState.swift in Sources */, BFB141181BE46934004FBF46 /* GameCollectionViewDataSource.swift in Sources */, + BF1FB1841C5EE643007E2494 /* SaveState+CoreDataProperties.swift in Sources */, BFFB709F1AF99B1700DE56FE /* EmulationViewController.swift in Sources */, BFAA1FF41B8AD7F900495943 /* ControllersSettingsViewController.swift in Sources */, BF353FF91C5D870B00C1184C /* PauseItem.swift in Sources */, @@ -719,6 +736,7 @@ BF797A2D1C2D339F00F1A000 /* UILabel+FontSize.swift in Sources */, BF3540021C5DA3D500C1184C /* PauseStoryboardSegue.swift in Sources */, BF107EC41BF413F000E0C32C /* GamesViewController.swift in Sources */, + BF172AEB1C68986300C26774 /* NSManagedObjectContext+Conveniences.swift in Sources */, BF5E7F441B9A650B00AE44F8 /* SettingsViewController.swift in Sources */, BF353FF21C5D7FB000C1184C /* PauseViewController.swift in Sources */, BFDB28451BC9DA7B001D0C83 /* GamePickerController.swift in Sources */, diff --git a/Delta/Base.lproj/PauseMenu.storyboard b/Delta/Base.lproj/PauseMenu.storyboard index 639f869..ca21423 100644 --- a/Delta/Base.lproj/PauseMenu.storyboard +++ b/Delta/Base.lproj/PauseMenu.storyboard @@ -1,7 +1,7 @@ - + - + @@ -144,11 +144,11 @@ - + - + @@ -170,7 +170,13 @@ - + + + + + + + diff --git a/Delta/Emulation/EmulationViewController.swift b/Delta/Emulation/EmulationViewController.swift index 9c93a3c..df1ed4e 100644 --- a/Delta/Emulation/EmulationViewController.swift +++ b/Delta/Emulation/EmulationViewController.swift @@ -39,7 +39,8 @@ class EmulationViewController: UIViewController return NSStringFromClass(presentationController.dynamicType).containsString("PreviewPresentation") } - private var _isPauseViewControllerPresented = false + private var pauseViewController: PauseViewController? + //MARK: - Initializers - @@ -120,7 +121,7 @@ class EmulationViewController: UIViewController coordinator.animateAlongsideTransition({ _ in - if self._isPauseViewControllerPresented + if self.pauseViewController != nil { // We need to manually "refresh" the game screen, otherwise the system tries to cache the rendered image, but skews it incorrectly when rotating b/c of UIVisualEffectView self.gameView.inputImage = self.gameView.outputImage @@ -148,11 +149,11 @@ class EmulationViewController: UIViewController } let saveStateItem = PauseItem(image: UIImage(named: "SmallPause")!, text: NSLocalizedString("Save State", comment: ""), action: { _ in - pauseViewController.presentSaveStateViewController() + pauseViewController.presentSaveStateViewController(delegate: self) }) let loadStateItem = PauseItem(image: UIImage(named: "SmallPause")!, text: NSLocalizedString("Load State", comment: ""), action: { _ in - pauseViewController.presentSaveStateViewController() + pauseViewController.presentSaveStateViewController(delegate: self) }) let cheatCodesItem = PauseItem(image: UIImage(named: "SmallPause")!, text: NSLocalizedString("Cheat Codes", comment: ""), action: dismissAction) @@ -165,13 +166,13 @@ class EmulationViewController: UIViewController pauseViewController.items = [saveStateItem, loadStateItem, cheatCodesItem, fastForwardItem, sustainButtonItem] - self._isPauseViewControllerPresented = true + self.pauseViewController = pauseViewController } } @IBAction func unwindFromPauseViewController(segue: UIStoryboardSegue) { - self._isPauseViewControllerPresented = false + self.pauseViewController = nil self.emulatorCore.resumeEmulation() } @@ -217,6 +218,46 @@ private extension EmulationViewController } } +//MARK: - Save States +/// Save States +extension EmulationViewController: SaveStatesViewControllerDelegate +{ + func saveStatesViewControllerActiveGame(saveStatesViewController: SaveStatesViewController) -> Game + { + return self.game + } + + func saveStatesViewController(saveStatesViewController: SaveStatesViewController, updateSaveState saveState: SaveState) + { + guard let filepath = saveState.fileURL.path else { return } + + self.emulatorCore.saveSaveState { temporarySaveState in + do + { + if NSFileManager.defaultManager().fileExistsAtPath(filepath) + { + try NSFileManager.defaultManager().replaceItemAtURL(saveState.fileURL, withItemAtURL: temporarySaveState.fileURL, backupItemName: nil, options: [], resultingItemURL: nil) + } + else + { + try NSFileManager.defaultManager().moveItemAtURL(temporarySaveState.fileURL, toURL: saveState.fileURL) + } + } + catch let error as NSError + { + print(error) + } + } + } + + func saveStatesViewController(saveStatesViewController: SaveStatesViewController, loadSaveState saveState: SaveState) + { + self.emulatorCore.loadSaveState(saveState) + + self.pauseViewController?.dismiss() + } +} + //MARK: - - /// extension EmulationViewController: GameControllerReceiverType diff --git a/Delta/Pause Menu/PauseViewController.swift b/Delta/Pause Menu/PauseViewController.swift index eff2887..047700e 100644 --- a/Delta/Pause Menu/PauseViewController.swift +++ b/Delta/Pause Menu/PauseViewController.swift @@ -16,6 +16,8 @@ class PauseViewController: UIViewController, PauseInfoProvidable /// var pauseText: String? = nil + private weak var saveStatesViewControllerDelegate: SaveStatesViewControllerDelegate? + /// UIViewController override var preferredContentSize: CGSize { set { } @@ -69,8 +71,9 @@ extension PauseViewController override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { - if segue.identifier == "embedNavigationController" + switch segue.identifier ?? "" { + case "embedNavigationController": self.pauseNavigationController = segue.destinationViewController as! UINavigationController self.pauseNavigationController.delegate = self self.pauseNavigationController.navigationBar.tintColor = UIColor.deltaLightPurpleColor() @@ -81,6 +84,12 @@ extension PauseViewController // Keep navigation bar outside the UIVisualEffectView's self.view.addSubview(self.pauseNavigationController.navigationBar) + + case "saveState": + let saveStatesViewController = segue.destinationViewController as! SaveStatesViewController + saveStatesViewController.delegate = self.saveStatesViewControllerDelegate + + default: break } } } @@ -92,8 +101,10 @@ extension PauseViewController self.performSegueWithIdentifier("unwindFromPauseMenu", sender: self) } - func presentSaveStateViewController() + func presentSaveStateViewController(delegate delegate: SaveStatesViewControllerDelegate) { + self.saveStatesViewControllerDelegate = delegate + self.performSegueWithIdentifier("saveState", sender: self) } } diff --git a/Delta/Pause Menu/Save States/SaveStatesViewController.swift b/Delta/Pause Menu/Save States/SaveStatesViewController.swift index 8065915..52c9f7f 100644 --- a/Delta/Pause Menu/Save States/SaveStatesViewController.swift +++ b/Delta/Pause Menu/Save States/SaveStatesViewController.swift @@ -7,17 +7,47 @@ // import UIKit +import CoreData +import DeltaCore import Roxas -private let SaveStatesViewControllerContentInset: CGFloat = 20 +protocol SaveStatesViewControllerDelegate: class +{ + func saveStatesViewControllerActiveGame(saveStatesViewController: SaveStatesViewController) -> Game + func saveStatesViewController(saveStatesViewController: SaveStatesViewController, updateSaveState saveState: SaveState) + func saveStatesViewController(saveStatesViewController: SaveStatesViewController, loadSaveState saveState: SaveState) +} class SaveStatesViewController: UICollectionViewController { + weak var delegate: SaveStatesViewControllerDelegate? + private var backgroundView: RSTBackgroundView! private var prototypeCell = GridCollectionViewCell() private var prototypeCellWidthConstraint: NSLayoutConstraint! + + private let fetchedResultsController: NSFetchedResultsController + + private let dateFormatter: NSDateFormatter + + required init?(coder aDecoder: NSCoder) + { + let fetchRequest = SaveState.fetchRequest() + fetchRequest.returnsObjectsAsFaults = false + fetchRequest.sortDescriptors = [NSSortDescriptor(key: SaveStateAttributes.creationDate.rawValue, ascending: true)] + + self.fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.sharedManager.managedObjectContext, sectionNameKeyPath: nil, cacheName: nil) + + self.dateFormatter = NSDateFormatter() + self.dateFormatter.timeStyle = .ShortStyle + self.dateFormatter.dateStyle = .ShortStyle + + super.init(coder: aDecoder) + + self.fetchedResultsController.delegate = self + } } extension SaveStatesViewController @@ -35,20 +65,38 @@ extension SaveStatesViewController self.backgroundView.detailTextLabel.textColor = UIColor.whiteColor() self.view.insertSubview(self.backgroundView, atIndex: 0) - // We update the layout in code because we need to use our SaveStatesViewControllerContentInset constant - // The reason for this is we cannot query the layout for its sectionInset in viewDidLayoutSubviews, so might as well be explicit in code with a constant - // Otherwise, we could configure this all in Interface Builder, but we'd still need to hardcode 20 in for viewDidLayoutSubviews let collectionViewLayout = self.collectionViewLayout as! GridCollectionViewLayout - collectionViewLayout.sectionInset = UIEdgeInsets(top: SaveStatesViewControllerContentInset, left: SaveStatesViewControllerContentInset, bottom: SaveStatesViewControllerContentInset, right: SaveStatesViewControllerContentInset) - collectionViewLayout.minimumInteritemSpacing = SaveStatesViewControllerContentInset - collectionViewLayout.minimumLineSpacing = SaveStatesViewControllerContentInset - + let averageHorizontalInset = (collectionViewLayout.sectionInset.left + collectionViewLayout.sectionInset.right) / 2 let portraitScreenWidth = UIScreen.mainScreen().coordinateSpace.convertRect(UIScreen.mainScreen().bounds, toCoordinateSpace: UIScreen.mainScreen().fixedCoordinateSpace).width - collectionViewLayout.itemWidth = (portraitScreenWidth - ((SaveStatesViewControllerContentInset) * 3)) / 2 + + // Use dimensions that allow two cells to fill the screen horizontally with padding in portrait mode + // We'll keep the same size for landscape orientation, which will allow more to fit + collectionViewLayout.itemWidth = (portraitScreenWidth - (averageHorizontalInset * 3)) / 2 // Manually update prototype cell properties self.prototypeCellWidthConstraint = self.prototypeCell.contentView.widthAnchor.constraintEqualToConstant(collectionViewLayout.itemWidth) self.prototypeCellWidthConstraint.active = true + + self.updateBackgroundView() + } + + override func viewWillAppear(animated: Bool) + { + if self.fetchedResultsController.fetchedObjects == nil + { + do + { + try self.fetchedResultsController.performFetch() + } + catch let error as NSError + { + print(error) + } + } + + self.updateBackgroundView() + + super.viewWillAppear(animated) } override func didReceiveMemoryWarning() @@ -57,10 +105,27 @@ extension SaveStatesViewController } } +private extension SaveStatesViewController +{ + func updateBackgroundView() + { + if let fetchedObjects = self.fetchedResultsController.fetchedObjects where fetchedObjects.count > 0 + { + self.backgroundView.hidden = true + } + else + { + self.backgroundView.hidden = false + } + } +} + private extension SaveStatesViewController { func configureCollectionViewCell(cell: GridCollectionViewCell, forIndexPath indexPath: NSIndexPath) { + let saveState = self.fetchedResultsController.objectAtIndexPath(indexPath) as! SaveState + cell.imageView.backgroundColor = UIColor.whiteColor() cell.imageView.image = UIImage(named: "DeltaPlaceholder") @@ -68,7 +133,42 @@ private extension SaveStatesViewController cell.textLabel.textColor = UIColor.whiteColor() cell.textLabel.font = UIFont.preferredFontForTextStyle(UIFontTextStyleSubheadline) - cell.textLabel.text = "Save State" + + let name = saveState.name ?? self.dateFormatter.stringFromDate(saveState.modifiedDate) + cell.textLabel.text = name + } +} + +private extension SaveStatesViewController +{ + @IBAction func addSaveState() + { + guard let delegate = self.delegate else { return } + + let backgroundContext = DatabaseManager.sharedManager.backgroundManagedObjectContext() + backgroundContext.performBlock { + + let identifier = NSUUID().UUIDString + let date = NSDate() + + var game = delegate.saveStatesViewControllerActiveGame(self) + game = backgroundContext.objectWithID(game.objectID) as! Game + + let saveState = SaveState.insertIntoManagedObjectContext(backgroundContext) + saveState.identifier = identifier + saveState.filename = identifier + saveState.creationDate = date + saveState.modifiedDate = date + saveState.game = game + + self.updateSaveState(saveState) + } + } + + func updateSaveState(saveState: SaveState) + { + self.delegate?.saveStatesViewController(self, updateSaveState: saveState) + saveState.managedObjectContext?.saveWithErrorLogging() } } @@ -76,7 +176,8 @@ extension SaveStatesViewController { override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return 12 + let section = self.fetchedResultsController.sections![section] + return section.numberOfObjects } override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell @@ -87,6 +188,15 @@ extension SaveStatesViewController } } +extension SaveStatesViewController +{ + override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) + { + let saveState = self.fetchedResultsController.objectAtIndexPath(indexPath) as! SaveState + self.delegate?.saveStatesViewController(self, loadSaveState: saveState) + } +} + extension SaveStatesViewController: UICollectionViewDelegateFlowLayout { func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize @@ -96,4 +206,13 @@ extension SaveStatesViewController: UICollectionViewDelegateFlowLayout let size = self.prototypeCell.contentView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize) return size } +} + +extension SaveStatesViewController: NSFetchedResultsControllerDelegate +{ + func controllerDidChangeContent(controller: NSFetchedResultsController) + { + self.collectionView?.reloadData() + self.updateBackgroundView() + } } \ No newline at end of file diff --git a/External/Roxas b/External/Roxas index 41ed3e9..6dc9067 160000 --- a/External/Roxas +++ b/External/Roxas @@ -1 +1 @@ -Subproject commit 41ed3e9fcc247258a83e103f8816dbc07652c75a +Subproject commit 6dc906703bb5870d268d341cefd1fdcc40001eb9