diff --git a/Common/Components/LoadImageOperation.swift b/Common/Components/LoadImageOperation.swift new file mode 100644 index 0000000..a01ec9f --- /dev/null +++ b/Common/Components/LoadImageOperation.swift @@ -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 + } + } +} \ No newline at end of file diff --git a/Common/Database/Model/SaveState.swift b/Common/Database/Model/SaveState.swift index 84c3d00..01f71f4 100644 --- a/Common/Database/Model/SaveState.swift +++ b/Common/Database/Model/SaveState.swift @@ -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 + } } diff --git a/Common/Extensions/NSOperationQueue+KeyValue.swift b/Common/Extensions/NSOperationQueue+KeyValue.swift new file mode 100644 index 0000000..4dd6ea5 --- /dev/null +++ b/Common/Extensions/NSOperationQueue+KeyValue.swift @@ -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 + } +} \ No newline at end of file diff --git a/Delta.xcodeproj/project.pbxproj b/Delta.xcodeproj/project.pbxproj index 420ddec..3f31856 100644 --- a/Delta.xcodeproj/project.pbxproj +++ b/Delta.xcodeproj/project.pbxproj @@ -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 = ""; }; 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 = ""; }; + BF0CDDAC1C8155D200640168 /* LoadImageOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = LoadImageOperation.swift; path = Components/LoadImageOperation.swift; sourceTree = ""; }; + BF0CDDAE1C81604100640168 /* NSOperationQueue+KeyValue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSOperationQueue+KeyValue.swift"; 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 = ""; }; @@ -314,6 +318,7 @@ children = ( BF762EAA1BC1B076002C8866 /* NSManagedObject+Conveniences.swift */, BF172AEA1C68986300C26774 /* NSManagedObjectContext+Conveniences.swift */, + BF0CDDAE1C81604100640168 /* NSOperationQueue+KeyValue.swift */, ); path = Extensions; sourceTree = ""; @@ -404,6 +409,7 @@ isa = PBXGroup; children = ( BFF1E5631BE04CAF000E9EF6 /* BoxArtImageView.swift */, + BF0CDDAC1C8155D200640168 /* LoadImageOperation.swift */, ); name = Components; sourceTree = ""; @@ -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 */, diff --git a/Delta/Emulation/EmulationViewController.swift b/Delta/Emulation/EmulationViewController.swift index df1ed4e..9d8ed33 100644 --- a/Delta/Emulation/EmulationViewController.swift +++ b/Delta/Emulation/EmulationViewController.swift @@ -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) diff --git a/Delta/Pause Menu/Save States/SaveStatesViewController.swift b/Delta/Pause Menu/Save States/SaveStatesViewController.swift index 52c9f7f..4578c02 100644 --- a/Delta/Pause Menu/Save States/SaveStatesViewController.swift +++ b/Delta/Pause Menu/Save States/SaveStatesViewController.swift @@ -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