Saves and displays thumbnails for save states
This commit is contained in:
parent
bf70df6611
commit
ee1e57f54b
67
Common/Components/LoadImageOperation.swift
Normal file
67
Common/Components/LoadImageOperation.swift
Normal file
@ -0,0 +1,67 @@
|
||||
//
|
||||
// LoadImageOperation.swift
|
||||
// Delta
|
||||
//
|
||||
// Created by Riley Testut on 2/26/16.
|
||||
// Copyright © 2016 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import ImageIO
|
||||
|
||||
public class LoadImageOperation: NSOperation
|
||||
{
|
||||
public let URL: NSURL
|
||||
|
||||
public var completionHandler: (UIImage? -> Void)?
|
||||
public var imageCache: NSCache?
|
||||
|
||||
public init(URL: NSURL)
|
||||
{
|
||||
self.URL = URL
|
||||
|
||||
super.init()
|
||||
}
|
||||
}
|
||||
|
||||
public extension LoadImageOperation
|
||||
{
|
||||
override func main()
|
||||
{
|
||||
var image: UIImage?
|
||||
|
||||
defer
|
||||
{
|
||||
if !self.cancelled
|
||||
{
|
||||
dispatch_async(dispatch_get_main_queue()) {
|
||||
self.completionHandler?(image)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
guard !self.cancelled else { return }
|
||||
|
||||
if let cachedImage = self.imageCache?.objectForKey(self.URL) as? UIImage
|
||||
{
|
||||
image = cachedImage
|
||||
return
|
||||
}
|
||||
|
||||
let options: NSDictionary = [kCGImageSourceShouldCache as NSString: true]
|
||||
|
||||
if let imageSource = CGImageSourceCreateWithURL(self.URL, options), quartzImage = CGImageSourceCreateImageAtIndex(imageSource, 0, options)
|
||||
{
|
||||
let loadedImage = UIImage(CGImage: quartzImage)
|
||||
|
||||
// Force decompression of image
|
||||
UIGraphicsBeginImageContextWithOptions(CGSize(width: 1, height: 1), true, 1.0)
|
||||
loadedImage.drawAtPoint(CGPoint.zero)
|
||||
UIGraphicsEndImageContext()
|
||||
|
||||
self.imageCache?.setObject(loadedImage, forKey: self.URL)
|
||||
|
||||
image = loadedImage
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -18,4 +18,10 @@ class SaveState: NSManagedObject, SaveStateType
|
||||
let fileURL = DatabaseManager.saveStatesDirectoryURLForGame(self.game).URLByAppendingPathComponent(self.filename)
|
||||
return fileURL
|
||||
}
|
||||
|
||||
var imageFileURL: NSURL {
|
||||
let imageFilename = (self.filename as NSString).stringByDeletingPathExtension + ".png"
|
||||
let imageFileURL = DatabaseManager.saveStatesDirectoryURLForGame(self.game).URLByAppendingPathComponent(imageFilename)
|
||||
return imageFileURL
|
||||
}
|
||||
}
|
||||
|
||||
41
Common/Extensions/NSOperationQueue+KeyValue.swift
Normal file
41
Common/Extensions/NSOperationQueue+KeyValue.swift
Normal file
@ -0,0 +1,41 @@
|
||||
//
|
||||
// NSOperationQueue+KeyValue.swift
|
||||
// Delta
|
||||
//
|
||||
// Created by Riley Testut on 2/26/16.
|
||||
// Copyright © 2016 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import ObjectiveC.runtime
|
||||
|
||||
|
||||
extension NSOperationQueue
|
||||
{
|
||||
private struct AssociatedKeys
|
||||
{
|
||||
static var OperationsDictionary = "delta_operationsDictionary"
|
||||
}
|
||||
|
||||
private var operationsDictionary: NSMapTable {
|
||||
get {
|
||||
return objc_getAssociatedObject(self, &AssociatedKeys.OperationsDictionary) as? NSMapTable ?? NSMapTable.strongToWeakObjectsMapTable()
|
||||
}
|
||||
|
||||
set {
|
||||
objc_setAssociatedObject(self, &AssociatedKeys.OperationsDictionary, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||||
}
|
||||
}
|
||||
|
||||
func addOperation(operation: NSOperation, forKey key: AnyObject)
|
||||
{
|
||||
self.operationsDictionary.objectForKey(key)
|
||||
self.addOperation(operation)
|
||||
}
|
||||
|
||||
func operationForKey(key: AnyObject) -> NSOperation?
|
||||
{
|
||||
let operation = self.operationsDictionary.objectForKey(key) as? NSOperation
|
||||
return operation
|
||||
}
|
||||
}
|
||||
@ -9,6 +9,8 @@
|
||||
/* Begin PBXBuildFile section */
|
||||
AF0535CD7331785FA15E0864 /* Pods.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 22506DA00971C4300AF90A35 /* Pods.framework */; };
|
||||
BF090CF41B490D8300DCAB45 /* UIDevice+Vibration.m in Sources */ = {isa = PBXBuildFile; fileRef = BF090CF31B490D8300DCAB45 /* UIDevice+Vibration.m */; };
|
||||
BF0CDDAD1C8155D200640168 /* LoadImageOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0CDDAC1C8155D200640168 /* LoadImageOperation.swift */; };
|
||||
BF0CDDAF1C81604100640168 /* NSOperationQueue+KeyValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0CDDAE1C81604100640168 /* NSOperationQueue+KeyValue.swift */; };
|
||||
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 */; };
|
||||
@ -132,6 +134,8 @@
|
||||
BF090CF11B490D8300DCAB45 /* Delta-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Delta-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
BF0CDDAC1C8155D200640168 /* LoadImageOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = LoadImageOperation.swift; path = Components/LoadImageOperation.swift; sourceTree = "<group>"; };
|
||||
BF0CDDAE1C81604100640168 /* NSOperationQueue+KeyValue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSOperationQueue+KeyValue.swift"; 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>"; };
|
||||
@ -314,6 +318,7 @@
|
||||
children = (
|
||||
BF762EAA1BC1B076002C8866 /* NSManagedObject+Conveniences.swift */,
|
||||
BF172AEA1C68986300C26774 /* NSManagedObjectContext+Conveniences.swift */,
|
||||
BF0CDDAE1C81604100640168 /* NSOperationQueue+KeyValue.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
@ -404,6 +409,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BFF1E5631BE04CAF000E9EF6 /* BoxArtImageView.swift */,
|
||||
BF0CDDAC1C8155D200640168 /* LoadImageOperation.swift */,
|
||||
);
|
||||
name = Components;
|
||||
sourceTree = "<group>";
|
||||
@ -717,6 +723,7 @@
|
||||
BF1FB1861C5EE643007E2494 /* SaveState.swift in Sources */,
|
||||
BFB141181BE46934004FBF46 /* GameCollectionViewDataSource.swift in Sources */,
|
||||
BF1FB1841C5EE643007E2494 /* SaveState+CoreDataProperties.swift in Sources */,
|
||||
BF0CDDAF1C81604100640168 /* NSOperationQueue+KeyValue.swift in Sources */,
|
||||
BFFB709F1AF99B1700DE56FE /* EmulationViewController.swift in Sources */,
|
||||
BFAA1FF41B8AD7F900495943 /* ControllersSettingsViewController.swift in Sources */,
|
||||
BF353FF91C5D870B00C1184C /* PauseItem.swift in Sources */,
|
||||
@ -735,6 +742,7 @@
|
||||
BF090CF41B490D8300DCAB45 /* UIDevice+Vibration.m in Sources */,
|
||||
BF797A2D1C2D339F00F1A000 /* UILabel+FontSize.swift in Sources */,
|
||||
BF3540021C5DA3D500C1184C /* PauseStoryboardSegue.swift in Sources */,
|
||||
BF0CDDAD1C8155D200640168 /* LoadImageOperation.swift in Sources */,
|
||||
BF107EC41BF413F000E0C32C /* GamesViewController.swift in Sources */,
|
||||
BF172AEB1C68986300C26774 /* NSManagedObjectContext+Conveniences.swift in Sources */,
|
||||
BF5E7F441B9A650B00AE44F8 /* SettingsViewController.swift in Sources */,
|
||||
|
||||
@ -41,8 +41,9 @@ class EmulationViewController: UIViewController
|
||||
|
||||
private var pauseViewController: PauseViewController?
|
||||
|
||||
private var context = CIContext(options: [kCIContextWorkingColorSpace: NSNull()])
|
||||
|
||||
|
||||
|
||||
//MARK: - Initializers -
|
||||
/** Initializers **/
|
||||
required init?(coder aDecoder: NSCoder)
|
||||
@ -148,12 +149,16 @@ class EmulationViewController: UIViewController
|
||||
pauseViewController.dismiss()
|
||||
}
|
||||
|
||||
let saveStateItem = PauseItem(image: UIImage(named: "SmallPause")!, text: NSLocalizedString("Save State", comment: ""), action: { _ in
|
||||
pauseViewController.presentSaveStateViewController(delegate: self)
|
||||
// Swift has a bug where using unowned references can lead to swift_abortRetainUnowned errors.
|
||||
// Specifically, if you pause a game, open the save states menu, go back, return to menu, select a new game, then try to pause it, it will crash
|
||||
// As a dirty workaround, we just use a weak reference, and force unwrap it if needed
|
||||
|
||||
let saveStateItem = PauseItem(image: UIImage(named: "SmallPause")!, text: NSLocalizedString("Save State", comment: ""), action: { [weak self] _ in
|
||||
pauseViewController.presentSaveStateViewController(delegate: self!)
|
||||
})
|
||||
|
||||
let loadStateItem = PauseItem(image: UIImage(named: "SmallPause")!, text: NSLocalizedString("Load State", comment: ""), action: { _ in
|
||||
pauseViewController.presentSaveStateViewController(delegate: self)
|
||||
let loadStateItem = PauseItem(image: UIImage(named: "SmallPause")!, text: NSLocalizedString("Load State", comment: ""), action: { [weak self] _ in
|
||||
pauseViewController.presentSaveStateViewController(delegate: self!)
|
||||
})
|
||||
|
||||
let cheatCodesItem = PauseItem(image: UIImage(named: "SmallPause")!, text: NSLocalizedString("Cheat Codes", comment: ""), action: dismissAction)
|
||||
@ -248,6 +253,14 @@ extension EmulationViewController: SaveStatesViewControllerDelegate
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
|
||||
if let outputImage = self.gameView.outputImage
|
||||
{
|
||||
let quartzImage = self.context.createCGImage(outputImage, fromRect: outputImage.extent)
|
||||
|
||||
let image = UIImage(CGImage: quartzImage)
|
||||
UIImagePNGRepresentation(image)?.writeToURL(saveState.imageFileURL, atomically: true)
|
||||
}
|
||||
}
|
||||
|
||||
func saveStatesViewController(saveStatesViewController: SaveStatesViewController, loadSaveState saveState: SaveState)
|
||||
|
||||
@ -21,32 +21,31 @@ protocol SaveStatesViewControllerDelegate: class
|
||||
|
||||
class SaveStatesViewController: UICollectionViewController
|
||||
{
|
||||
weak var delegate: SaveStatesViewControllerDelegate?
|
||||
weak var delegate: SaveStatesViewControllerDelegate! {
|
||||
didSet {
|
||||
self.updateFetchedResultsController()
|
||||
}
|
||||
}
|
||||
|
||||
private var backgroundView: RSTBackgroundView!
|
||||
|
||||
private var prototypeCell = GridCollectionViewCell()
|
||||
private var prototypeCellWidthConstraint: NSLayoutConstraint!
|
||||
|
||||
private let fetchedResultsController: NSFetchedResultsController
|
||||
private var fetchedResultsController: NSFetchedResultsController!
|
||||
|
||||
private let imageOperationQueue = NSOperationQueue()
|
||||
private let imageCache = NSCache()
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@ -107,6 +106,19 @@ extension SaveStatesViewController
|
||||
|
||||
private extension SaveStatesViewController
|
||||
{
|
||||
func updateFetchedResultsController()
|
||||
{
|
||||
let game = self.delegate.saveStatesViewControllerActiveGame(self)
|
||||
|
||||
let fetchRequest = SaveState.fetchRequest()
|
||||
fetchRequest.returnsObjectsAsFaults = false
|
||||
fetchRequest.predicate = NSPredicate(format: "%K == %@", SaveStateAttributes.game.rawValue, game)
|
||||
fetchRequest.sortDescriptors = [NSSortDescriptor(key: SaveStateAttributes.creationDate.rawValue, ascending: true)]
|
||||
|
||||
self.fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.sharedManager.managedObjectContext, sectionNameKeyPath: nil, cacheName: nil)
|
||||
self.fetchedResultsController.delegate = self
|
||||
}
|
||||
|
||||
func updateBackgroundView()
|
||||
{
|
||||
if let fetchedObjects = self.fetchedResultsController.fetchedObjects where fetchedObjects.count > 0
|
||||
@ -129,7 +141,17 @@ private extension SaveStatesViewController
|
||||
cell.imageView.backgroundColor = UIColor.whiteColor()
|
||||
cell.imageView.image = UIImage(named: "DeltaPlaceholder")
|
||||
|
||||
cell.maximumImageSize = CGSizeMake(self.prototypeCellWidthConstraint.constant, (self.prototypeCellWidthConstraint.constant / 4.0) * 3.0)
|
||||
let imageOperation = LoadImageOperation(URL: saveState.imageFileURL)
|
||||
imageOperation.completionHandler = { image in
|
||||
if let image = image
|
||||
{
|
||||
cell.imageView.image = image
|
||||
}
|
||||
}
|
||||
|
||||
self.imageOperationQueue.addOperation(imageOperation, forKey: indexPath)
|
||||
|
||||
cell.maximumImageSize = CGSizeMake(self.prototypeCellWidthConstraint.constant, (self.prototypeCellWidthConstraint.constant / 8.0) * 7.0)
|
||||
|
||||
cell.textLabel.textColor = UIColor.whiteColor()
|
||||
cell.textLabel.font = UIFont.preferredFontForTextStyle(UIFontTextStyleSubheadline)
|
||||
@ -143,15 +165,13 @@ 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)
|
||||
var game = self.delegate.saveStatesViewControllerActiveGame(self)
|
||||
game = backgroundContext.objectWithID(game.objectID) as! Game
|
||||
|
||||
let saveState = SaveState.insertIntoManagedObjectContext(backgroundContext)
|
||||
@ -195,6 +215,12 @@ extension SaveStatesViewController
|
||||
let saveState = self.fetchedResultsController.objectAtIndexPath(indexPath) as! SaveState
|
||||
self.delegate?.saveStatesViewController(self, loadSaveState: saveState)
|
||||
}
|
||||
|
||||
override func collectionView(collectionView: UICollectionView, didEndDisplayingCell cell: UICollectionViewCell, forItemAtIndexPath indexPath: NSIndexPath)
|
||||
{
|
||||
let operation = self.imageOperationQueue.operationForKey(indexPath)
|
||||
operation?.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
extension SaveStatesViewController: UICollectionViewDelegateFlowLayout
|
||||
|
||||
Loading…
Reference in New Issue
Block a user