diff --git a/Common/Components/LoadImageOperation.swift b/Common/Components/LoadImageOperation.swift index a01ec9f..0c300d5 100644 --- a/Common/Components/LoadImageOperation.swift +++ b/Common/Components/LoadImageOperation.swift @@ -9,12 +9,30 @@ import Foundation import ImageIO -public class LoadImageOperation: NSOperation +import Roxas + +public class LoadImageOperation: RSTOperation { public let URL: NSURL - public var completionHandler: (UIImage? -> Void)? - public var imageCache: NSCache? + public var completionHandler: (UIImage? -> Void)? { + didSet { + self.completionBlock = { + rst_dispatch_sync_on_main_thread() { + self.completionHandler?(self.image) + } + } + } + } + + public var imageCache: NSCache? { + didSet { + // Ensures if an image is cached, it will be returned immediately, to prevent temporary flash of placeholder image + self.immediate = self.imageCache?.objectForKey(self.URL) != nil + } + } + + private var image: UIImage? public init(URL: NSURL) { @@ -28,23 +46,11 @@ 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 + self.image = cachedImage return } @@ -61,7 +67,7 @@ public extension LoadImageOperation self.imageCache?.setObject(loadedImage, forKey: self.URL) - image = loadedImage + self.image = loadedImage } } } \ No newline at end of file diff --git a/Common/Extensions/NSOperationQueue+KeyValue.swift b/Common/Extensions/NSOperationQueue+KeyValue.swift deleted file mode 100644 index 4dd6ea5..0000000 --- a/Common/Extensions/NSOperationQueue+KeyValue.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// 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 3f31856..a220e93 100644 --- a/Delta.xcodeproj/project.pbxproj +++ b/Delta.xcodeproj/project.pbxproj @@ -10,7 +10,6 @@ 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 */; }; @@ -135,7 +134,6 @@ 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 = ""; }; @@ -318,7 +316,6 @@ children = ( BF762EAA1BC1B076002C8866 /* NSManagedObject+Conveniences.swift */, BF172AEA1C68986300C26774 /* NSManagedObjectContext+Conveniences.swift */, - BF0CDDAE1C81604100640168 /* NSOperationQueue+KeyValue.swift */, ); path = Extensions; sourceTree = ""; @@ -723,7 +720,6 @@ 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 */, diff --git a/Delta.xcodeproj/xcshareddata/xcschemes/Delta.xcscheme b/Delta.xcodeproj/xcshareddata/xcschemes/Delta.xcscheme new file mode 100644 index 0000000..b6554e7 --- /dev/null +++ b/Delta.xcodeproj/xcshareddata/xcschemes/Delta.xcscheme @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Delta/Pause Menu/Save States/SaveStatesViewController.swift b/Delta/Pause Menu/Save States/SaveStatesViewController.swift index ab9717e..36d505e 100644 --- a/Delta/Pause Menu/Save States/SaveStatesViewController.swift +++ b/Delta/Pause Menu/Save States/SaveStatesViewController.swift @@ -45,7 +45,7 @@ class SaveStatesViewController: UICollectionViewController private var fetchedResultsController: NSFetchedResultsController! - private let imageOperationQueue = NSOperationQueue() + private let imageOperationQueue = RSTOperationQueue() private let imageCache = NSCache() private let dateFormatter: NSDateFormatter @@ -122,7 +122,7 @@ extension SaveStatesViewController super.viewWillAppear(animated) } - + override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() @@ -157,26 +157,36 @@ private extension SaveStatesViewController self.backgroundView.hidden = false } } -} - -private extension SaveStatesViewController -{ - func configureCollectionViewCell(cell: GridCollectionViewCell, forIndexPath indexPath: NSIndexPath) + + //MARK: - Configure Cell - + + func configureCollectionViewCell(cell: GridCollectionViewCell, forIndexPath indexPath: NSIndexPath, ignoreExpensiveOperations ignoreOperations: Bool = false) { let saveState = self.fetchedResultsController.objectAtIndexPath(indexPath) as! SaveState cell.imageView.backgroundColor = UIColor.whiteColor() cell.imageView.image = UIImage(named: "DeltaPlaceholder") - let imageOperation = LoadImageOperation(URL: saveState.imageFileURL) - imageOperation.completionHandler = { image in - if let image = image - { - cell.imageView.image = image + if !ignoreOperations + { + let imageOperation = LoadImageOperation(URL: saveState.imageFileURL) + imageOperation.imageCache = self.imageCache + imageOperation.completionHandler = { image in + + if let image = image + { + cell.imageView.image = image + } } - } - - self.imageOperationQueue.addOperation(imageOperation, forKey: indexPath) + + // Ensure initially visible cells have loaded their image before they appear to prevent potential flickering from placeholder to thumbnail + if self.appearing + { + imageOperation.immediate = true + } + + self.imageOperationQueue.addOperation(imageOperation, forKey: indexPath) + } cell.maximumImageSize = CGSizeMake(self.prototypeCellWidthConstraint.constant, (self.prototypeCellWidthConstraint.constant / 8.0) * 7.0) @@ -293,7 +303,8 @@ extension SaveStatesViewController: UICollectionViewDelegateFlowLayout { func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize { - self.configureCollectionViewCell(self.prototypeCell, forIndexPath: indexPath) + // No need to load images from disk just to determine size, so we pass true for ignoreExpensiveOperations + self.configureCollectionViewCell(self.prototypeCell, forIndexPath: indexPath, ignoreExpensiveOperations: true) let size = self.prototypeCell.contentView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize) return size diff --git a/External/Roxas b/External/Roxas index 6dc9067..c795d0a 160000 --- a/External/Roxas +++ b/External/Roxas @@ -1 +1 @@ -Subproject commit 6dc906703bb5870d268d341cefd1fdcc40001eb9 +Subproject commit c795d0ace22cfe57c3b73613630e14b58d2f8f09