Adds basic support for saving + loading save states

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.
This commit is contained in:
Riley Testut 2016-02-08 13:53:46 -08:00
parent 544f1a1801
commit bf70df6611
14 changed files with 390 additions and 76 deletions

View File

@ -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)
}
}
}

View File

@ -20,8 +20,8 @@ enum GameCollectionAttributes: String
case games
}
extension GameCollection {
extension GameCollection
{
@NSManaged var identifier: String
@NSManaged var index: Int16

View File

@ -1,12 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model userDefinedModelVersionIdentifier="1.0" type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="9057" systemVersion="15B42" minimumToolsVersion="Xcode 7.0">
<model userDefinedModelVersionIdentifier="1.0" type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="9525" systemVersion="15D21" minimumToolsVersion="Xcode 7.0">
<entity name="Game" representedClassName="Game" syncable="YES">
<attribute name="artworkURL" optional="YES" attributeType="Transformable" syncable="YES">
<userInfo>
<entry key="attributeValueClassName" value="NSURL"/>
</userInfo>
</attribute>
<attribute name="filename" optional="YES" attributeType="String" syncable="YES">
<attribute name="filename" attributeType="String" syncable="YES">
<userInfo>
<entry key="attributeValueClassName" value="NSURL"/>
</userInfo>
@ -15,6 +15,7 @@
<attribute name="name" attributeType="String" syncable="YES"/>
<attribute name="typeIdentifier" attributeType="String" syncable="YES"/>
<relationship name="gameCollections" toMany="YES" deletionRule="Nullify" destinationEntity="GameCollection" inverseName="games" inverseEntity="GameCollection" syncable="YES"/>
<relationship name="saveStates" toMany="YES" deletionRule="Cascade" destinationEntity="SaveState" inverseName="game" inverseEntity="SaveState" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
@ -31,8 +32,26 @@
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="SaveState" representedClassName="SaveState" syncable="YES">
<attribute name="creationDate" attributeType="Date" syncable="YES"/>
<attribute name="filename" attributeType="String" syncable="YES">
<userInfo>
<entry key="attributeValueClassName" value="NSURL"/>
</userInfo>
</attribute>
<attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="modifiedDate" attributeType="Date" syncable="YES"/>
<attribute name="name" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="game" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Game" inverseName="saveStates" inverseEntity="Game" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<elements>
<element name="Game" positionX="-200" positionY="9" width="128" height="135"/>
<element name="Game" positionX="-200" positionY="-72" width="128" height="148"/>
<element name="GameCollection" positionX="-198" positionY="-207" width="128" height="90"/>
<element name="SaveState" positionX="-198" positionY="113" width="128" height="135"/>
</elements>
</model>

View File

@ -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!
}

View File

@ -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
}
}

View File

@ -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)
}
}
}

@ -1 +1 @@
Subproject commit d5b6ac78401dc1c9bab50bbf636f42b1c0e9fe7c
Subproject commit 23ce4c9b10760ad88d5c4c6703e88f6868a400df

@ -1 +1 @@
Subproject commit f4e27814258993234883fbca040ea66dc1d9609e
Subproject commit 6d9dbe986cdacd0cf2602035608188bcdf2c178a

View File

@ -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 = "<group>"; };
BF090CF31B490D8300DCAB45 /* UIDevice+Vibration.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIDevice+Vibration.m"; sourceTree = "<group>"; };
BF107EC31BF413F000E0C32C /* GamesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GamesViewController.swift; sourceTree = "<group>"; };
BF172AEA1C68986300C26774 /* NSManagedObjectContext+Conveniences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext+Conveniences.swift"; sourceTree = "<group>"; };
BF1FB1821C5EE643007E2494 /* SaveState+CoreDataProperties.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SaveState+CoreDataProperties.swift"; sourceTree = "<group>"; };
BF1FB1831C5EE643007E2494 /* SaveState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SaveState.swift; sourceTree = "<group>"; };
BF27CC861BC9E3C600A20D89 /* Delta.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = Delta.entitlements; sourceTree = "<group>"; };
BF27CC8A1BC9FE4D00A20D89 /* Pods.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Pods.framework; path = "Pods/../build/Debug-appletvos/Pods.framework"; sourceTree = "<group>"; };
BF27CC901BCB156200A20D89 /* EmulationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmulationViewController.swift; sourceTree = "<group>"; };
@ -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 = "<group>";
@ -302,6 +313,7 @@
isa = PBXGroup;
children = (
BF762EAA1BC1B076002C8866 /* NSManagedObject+Conveniences.swift */,
BF172AEA1C68986300C26774 /* NSManagedObjectContext+Conveniences.swift */,
);
path = Extensions;
sourceTree = "<group>";
@ -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 */,

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="9531" systemVersion="15D21" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="Dt0-nV-isV">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="9532" systemVersion="15D21" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="Dt0-nV-isV">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="9529"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="9530"/>
</dependencies>
<scenes>
<!--Pause View Controller-->
@ -144,11 +144,11 @@
<scene sceneID="9Fi-Ti-W8T">
<objects>
<collectionViewController storyboardIdentifier="saveStatesViewController" id="OOk-k7-INg" customClass="SaveStatesViewController" customModule="Delta" customModuleProvider="target" sceneMemberID="viewController">
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" dataMode="prototypes" id="XgF-OF-CVf">
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" id="XgF-OF-CVf">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="10" minimumInteritemSpacing="10" id="tvW-q1-PD8" customClass="GridCollectionViewLayout" customModule="Delta" customModuleProvider="target">
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="20" minimumInteritemSpacing="20" id="tvW-q1-PD8" customClass="GridCollectionViewLayout" customModule="Delta" customModuleProvider="target">
<size key="itemSize" width="50" height="50"/>
<size key="headerReferenceSize" width="0.0" height="0.0"/>
<size key="footerReferenceSize" width="0.0" height="0.0"/>
@ -170,7 +170,13 @@
<outlet property="delegate" destination="OOk-k7-INg" id="aLg-5i-MAd"/>
</connections>
</collectionView>
<navigationItem key="navigationItem" title="Save State" id="BoG-k2-aR2"/>
<navigationItem key="navigationItem" title="Save State" id="BoG-k2-aR2">
<barButtonItem key="rightBarButtonItem" systemItem="add" id="lKg-Ks-hWN">
<connections>
<action selector="addSaveState" destination="OOk-k7-INg" id="xY2-94-EOr"/>
</connections>
</barButtonItem>
</navigationItem>
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" barStyle="black" prompted="NO"/>
</collectionViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="cL5-DH-K3b" userLabel="First Responder" sceneMemberID="firstResponder"/>

View File

@ -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: - <GameControllerReceiver> -
/// <GameControllerReceiver>
extension EmulationViewController: GameControllerReceiverType

View File

@ -16,6 +16,8 @@ class PauseViewController: UIViewController, PauseInfoProvidable
/// <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)
}
}

View File

@ -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()
}
}

2
External/Roxas vendored

@ -1 +1 @@
Subproject commit 41ed3e9fcc247258a83e103f8816dbc07652c75a
Subproject commit 6dc906703bb5870d268d341cefd1fdcc40001eb9