diff --git a/Common/Collection View/GridCollectionViewLayout.swift b/Common/Collection View/GridCollectionViewLayout.swift index 048ee1e..e2351ef 100644 --- a/Common/Collection View/GridCollectionViewLayout.swift +++ b/Common/Collection View/GridCollectionViewLayout.swift @@ -49,6 +49,8 @@ class GridCollectionViewLayout: UICollectionViewFlowLayout for (index, attributes) in layoutAttributes.enumerate() { + guard attributes.representedElementCategory == .Cell else { continue } + // Ensure equal spacing between items (that also match the section insets) if index > 0 { diff --git a/Common/Database/Model/Model.xcdatamodeld/Model.xcdatamodel/contents b/Common/Database/Model/Model.xcdatamodeld/Model.xcdatamodel/contents index 64dc8e4..430ddf7 100644 --- a/Common/Database/Model/Model.xcdatamodeld/Model.xcdatamodel/contents +++ b/Common/Database/Model/Model.xcdatamodeld/Model.xcdatamodel/contents @@ -42,6 +42,7 @@ + @@ -52,6 +53,6 @@ - + \ No newline at end of file diff --git a/Common/Database/Model/SaveState.swift b/Common/Database/Model/SaveState.swift index 6f324bd..b1fdf53 100644 --- a/Common/Database/Model/SaveState.swift +++ b/Common/Database/Model/SaveState.swift @@ -20,9 +20,17 @@ extension SaveState case name case creationDate case modifiedDate + case type case game } + + @objc enum Type: Int16 + { + case Auto + case General + case Locked + } } @objc(SaveState) @@ -30,6 +38,7 @@ class SaveState: NSManagedObject, SaveStateType { @NSManaged var name: String? @NSManaged var modifiedDate: NSDate + @NSManaged var type: Type @NSManaged private(set) var filename: String @NSManaged private(set) var identifier: String diff --git a/Delta.xcodeproj/project.pbxproj b/Delta.xcodeproj/project.pbxproj index 70c4706..265f80c 100644 --- a/Delta.xcodeproj/project.pbxproj +++ b/Delta.xcodeproj/project.pbxproj @@ -27,6 +27,7 @@ BF2A53FC1BB74FC10052BD0C /* SNESDeltaCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BFC134E01AAD82460087AD7B /* SNESDeltaCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; BF2A53FD1BB74FC60052BD0C /* ZipZap.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF70798B1B6B464B0019077C /* ZipZap.framework */; }; BF2A53FE1BB74FC60052BD0C /* ZipZap.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BF70798B1B6B464B0019077C /* ZipZap.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + BF2B98E61C97E32F00F6D57D /* SaveStatesCollectionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF2B98E51C97E32F00F6D57D /* SaveStatesCollectionHeaderView.swift */; }; BF353FF21C5D7FB000C1184C /* PauseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF353FF11C5D7FB000C1184C /* PauseViewController.swift */; }; BF353FF31C5D7FB000C1184C /* PauseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF353FF11C5D7FB000C1184C /* PauseViewController.swift */; }; BF353FF61C5D837600C1184C /* PauseMenu.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BF353FF41C5D837600C1184C /* PauseMenu.storyboard */; }; @@ -136,6 +137,7 @@ BF27CC901BCB156200A20D89 /* EmulationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmulationViewController.swift; sourceTree = ""; }; BF27CC941BCB7B7A00A20D89 /* GameController.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = GameController.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS9.0.sdk/System/Library/Frameworks/GameController.framework; sourceTree = DEVELOPER_DIR; }; BF27CC961BCC890700A20D89 /* GamesCollectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GamesCollectionViewController.swift; sourceTree = ""; }; + BF2B98E51C97E32F00F6D57D /* SaveStatesCollectionHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SaveStatesCollectionHeaderView.swift; path = "Pause Menu/Save States/SaveStatesCollectionHeaderView.swift"; sourceTree = ""; }; BF353FF11C5D7FB000C1184C /* PauseViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PauseViewController.swift; path = "Pause Menu/PauseViewController.swift"; sourceTree = ""; }; BF353FF51C5D837600C1184C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/PauseMenu.storyboard; sourceTree = ""; }; BF353FF81C5D870B00C1184C /* PauseItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PauseItem.swift; path = "Pause Menu/PauseItem.swift"; sourceTree = ""; }; @@ -242,6 +244,7 @@ isa = PBXGroup; children = ( BF3540041C5DA70400C1184C /* SaveStatesViewController.swift */, + BF2B98E51C97E32F00F6D57D /* SaveStatesCollectionHeaderView.swift */, ); name = "Save States"; sourceTree = ""; @@ -714,6 +717,7 @@ BFDE393C1BC0CEDF003F72E8 /* Game.swift in Sources */, BFC2731A1BE6152200D22B05 /* GameCollection.swift in Sources */, BFF1E5641BE04CAF000E9EF6 /* BoxArtImageView.swift in Sources */, + BF2B98E61C97E32F00F6D57D /* SaveStatesCollectionHeaderView.swift in Sources */, BF762EAB1BC1B076002C8866 /* NSManagedObject+Conveniences.swift in Sources */, BF353FFF1C5DA3C500C1184C /* PausePresentationController.swift in Sources */, BF7AE80A1C2E8C7600B1B5BC /* UIColor+Delta.swift in Sources */, diff --git a/Delta/Base.lproj/PauseMenu.storyboard b/Delta/Base.lproj/PauseMenu.storyboard index ca21423..702513e 100644 --- a/Delta/Base.lproj/PauseMenu.storyboard +++ b/Delta/Base.lproj/PauseMenu.storyboard @@ -150,13 +150,13 @@ - + - + - + @@ -165,6 +165,10 @@ + + + + diff --git a/Delta/Pause Menu/Save States/SaveStatesCollectionHeaderView.swift b/Delta/Pause Menu/Save States/SaveStatesCollectionHeaderView.swift new file mode 100644 index 0000000..31dda2b --- /dev/null +++ b/Delta/Pause Menu/Save States/SaveStatesCollectionHeaderView.swift @@ -0,0 +1,45 @@ +// +// SaveStatesCollectionHeaderView.swift +// Delta +// +// Created by Riley Testut on 3/15/16. +// Copyright © 2016 Riley Testut. All rights reserved. +// + +import UIKit + +class SaveStatesCollectionHeaderView: UICollectionReusableView +{ + let textLabel = UILabel() + + override init(frame: CGRect) + { + super.init(frame: frame) + + self.initialize() + } + + required init?(coder aDecoder: NSCoder) + { + super.init(coder: aDecoder) + + self.initialize() + } + + private func initialize() + { + self.textLabel.translatesAutoresizingMaskIntoConstraints = false + self.textLabel.textColor = UIColor.whiteColor() + + var fontDescriptor = UIFontDescriptor.preferredFontDescriptorWithTextStyle(UIFontTextStyleTitle3) + fontDescriptor = fontDescriptor.fontDescriptorWithSymbolicTraits([.TraitBold]) + + self.textLabel.font = UIFont(descriptor: fontDescriptor, size: 0.0) + self.textLabel.textAlignment = .Center + self.addSubview(self.textLabel) + + // Auto Layout + NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-20-[textLabel]-20-|", options: [], metrics: nil, views: ["textLabel": self.textLabel])) + NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-10-[textLabel]|", options: [], metrics: nil, views: ["textLabel": self.textLabel])) + } +} diff --git a/Delta/Pause Menu/Save States/SaveStatesViewController.swift b/Delta/Pause Menu/Save States/SaveStatesViewController.swift index 04636c0..04a023a 100644 --- a/Delta/Pause Menu/Save States/SaveStatesViewController.swift +++ b/Delta/Pause Menu/Save States/SaveStatesViewController.swift @@ -26,6 +26,13 @@ extension SaveStatesViewController case Saving case Loading } + + enum Section: Int + { + case Auto + case General + case Locked + } } class SaveStatesViewController: UICollectionViewController @@ -42,6 +49,7 @@ class SaveStatesViewController: UICollectionViewController private var prototypeCell = GridCollectionViewCell() private var prototypeCellWidthConstraint: NSLayoutConstraint! + private var prototypeHeader = SaveStatesCollectionHeaderView() private var fetchedResultsController: NSFetchedResultsController! @@ -140,9 +148,9 @@ private extension SaveStatesViewController let fetchRequest = SaveState.fetchRequest() fetchRequest.returnsObjectsAsFaults = false fetchRequest.predicate = NSPredicate(format: "%K == %@", SaveState.Attributes.game.rawValue, game) - fetchRequest.sortDescriptors = [NSSortDescriptor(key: SaveState.Attributes.creationDate.rawValue, ascending: true)] + fetchRequest.sortDescriptors = [NSSortDescriptor(key: SaveState.Attributes.type.rawValue, ascending: true), NSSortDescriptor(key: SaveState.Attributes.creationDate.rawValue, ascending: true)] - self.fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.sharedManager.managedObjectContext, sectionNameKeyPath: nil, cacheName: nil) + self.fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.sharedManager.managedObjectContext, sectionNameKeyPath: SaveState.Attributes.type.rawValue, cacheName: nil) self.fetchedResultsController.delegate = self } @@ -158,7 +166,7 @@ private extension SaveStatesViewController } } - //MARK: - Configure Cell - + //MARK: - Configure Views - func configureCollectionViewCell(cell: GridCollectionViewCell, forIndexPath indexPath: NSIndexPath, ignoreExpensiveOperations ignoreOperations: Bool = false) { @@ -175,6 +183,7 @@ private extension SaveStatesViewController if let image = image { + cell.imageView.backgroundColor = nil cell.imageView.image = image } } @@ -197,6 +206,22 @@ private extension SaveStatesViewController cell.textLabel.text = name } + func configureCollectionViewHeaderView(headerView: SaveStatesCollectionHeaderView, forSection section: Int) + { + let section = self.correctedSectionForSectionIndex(section) + + let title: String + + switch section + { + case .Auto: title = NSLocalizedString("Auto Save", comment: "") + case .General: title = NSLocalizedString("General", comment: "") + case .Locked: title = NSLocalizedString("Locked", comment: "") + } + + headerView.textLabel.text = title + } + //MARK: - Gestures - @objc func handleLongPressGesture(gestureRecognizer: UILongPressGestureRecognizer) @@ -205,12 +230,29 @@ private extension SaveStatesViewController guard let indexPath = self.collectionView?.indexPathForItemAtPoint(gestureRecognizer.locationInView(self.collectionView)) else { return } + let saveState = self.fetchedResultsController.objectAtIndexPath(indexPath) as! SaveState + let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .ActionSheet) + alertController.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .Cancel, handler: nil)) alertController.addAction(UIAlertAction(title: NSLocalizedString("Delete Save State", comment: ""), style: .Destructive, handler: { action in - let saveState = self.fetchedResultsController.objectAtIndexPath(indexPath) as! SaveState self.deleteSaveState(saveState) })) - alertController.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .Cancel, handler: nil)) + + let section = self.correctedSectionForSectionIndex(indexPath.section) + switch section + { + case .Auto: break + case .General: + alertController.addAction(UIAlertAction(title: NSLocalizedString("Lock Save State", comment: ""), style: .Default, handler: { action in + self.lockSaveState(saveState) + })) + + case .Locked: + alertController.addAction(UIAlertAction(title: NSLocalizedString("Unlock Save State", comment: ""), style: .Default, handler: { action in + self.unlockSaveState(saveState) + })) + + } self.presentViewController(alertController, animated: true, completion: nil) } @@ -261,11 +303,48 @@ private extension SaveStatesViewController self.presentViewController(confirmationAlertController, animated: true, completion: nil) } + + func lockSaveState(saveState: SaveState) + { + let backgroundContext = DatabaseManager.sharedManager.backgroundManagedObjectContext() + backgroundContext.performBlockAndWait() { + let temporarySaveState = backgroundContext.objectWithID(saveState.objectID) as! SaveState + temporarySaveState.type = .Locked + backgroundContext.saveWithErrorLogging() + } + } + + func unlockSaveState(saveState: SaveState) + { + let backgroundContext = DatabaseManager.sharedManager.backgroundManagedObjectContext() + backgroundContext.performBlockAndWait() { + let temporarySaveState = backgroundContext.objectWithID(saveState.objectID) as! SaveState + temporarySaveState.type = .General + backgroundContext.saveWithErrorLogging() + } + } + + //MARK: - Convenience Methods - + + func correctedSectionForSectionIndex(section: Int) -> Section + { + let sectionInfo = self.fetchedResultsController.sections![section] + let sectionIndex = Int(sectionInfo.name)! + + let section = Section(rawValue: sectionIndex)! + return section + } } //MARK: - - extension SaveStatesViewController { + override func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int + { + let numberOfSections = self.fetchedResultsController.sections!.count + return numberOfSections + } + override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { let section = self.fetchedResultsController.sections![section] @@ -278,6 +357,13 @@ extension SaveStatesViewController self.configureCollectionViewCell(cell, forIndexPath: indexPath) return cell } + + override func collectionView(collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionReusableView + { + let headerView = collectionView.dequeueReusableSupplementaryViewOfKind(kind, withReuseIdentifier: "Header", forIndexPath: indexPath) as! SaveStatesCollectionHeaderView + self.configureCollectionViewHeaderView(headerView, forSection: indexPath.section) + return headerView + } } //MARK: - - @@ -289,7 +375,26 @@ extension SaveStatesViewController switch self.mode { - case .Saving: self.updateSaveState(saveState) + case .Saving: + + let section = self.correctedSectionForSectionIndex(indexPath.section) + switch section + { + case .Auto: break + case .General: + let backgroundContext = DatabaseManager.sharedManager.backgroundManagedObjectContext() + backgroundContext.performBlockAndWait() { + let temporarySaveState = backgroundContext.objectWithID(saveState.objectID) as! SaveState + self.updateSaveState(temporarySaveState) + } + + case .Locked: + let alertController = UIAlertController(title: NSLocalizedString("Cannot Modify Locked Save State", comment: ""), message: NSLocalizedString("This save state must first be unlocked before it can be modified.", comment: ""), preferredStyle: .Alert) + alertController.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .Cancel, handler: nil)) + self.presentViewController(alertController, animated: true, completion: nil) + + } + case .Loading: self.loadSaveState(saveState) } } @@ -312,6 +417,14 @@ extension SaveStatesViewController: UICollectionViewDelegateFlowLayout let size = self.prototypeCell.contentView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize) return size } + + func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize + { + self.configureCollectionViewHeaderView(self.prototypeHeader, forSection: section) + + let size = self.prototypeHeader.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize) + return size + } } //MARK: - -