From 7664c72f12b442404ce911299f39f381f2519ac1 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Mon, 7 Nov 2016 14:32:12 -0800 Subject: [PATCH] Adds ControllerSkinsViewController Displays all controller skins that support provided Traits --- .../LoadControllerSkinImageOperation.swift | 60 ++++++ Common/Components/LoadImageOperation.swift | 54 +++-- Common/Components/LoadImageURLOperation.swift | 33 +++ Common/Database/Model/ControllerSkin.swift | 9 + Delta.xcodeproj/project.pbxproj | 18 +- .../SaveStatesViewController.swift | 2 +- .../ControllerSkinTableViewCell.swift | 26 +++ .../ControllerSkinsViewController.swift | 199 ++++++++++++++++++ ...ameTypeControllerSkinsViewController.swift | 26 +++ Delta/Settings/Settings.storyboard | 124 ++++++++--- External/Roxas | 2 +- 11 files changed, 487 insertions(+), 66 deletions(-) create mode 100644 Common/Components/LoadControllerSkinImageOperation.swift create mode 100644 Common/Components/LoadImageURLOperation.swift create mode 100644 Delta/Settings/Controller Skins/ControllerSkinTableViewCell.swift create mode 100644 Delta/Settings/Controller Skins/ControllerSkinsViewController.swift diff --git a/Common/Components/LoadControllerSkinImageOperation.swift b/Common/Components/LoadControllerSkinImageOperation.swift new file mode 100644 index 0000000..10ffd96 --- /dev/null +++ b/Common/Components/LoadControllerSkinImageOperation.swift @@ -0,0 +1,60 @@ +// +// LoadControllerSkinImageOperation.swift +// Delta +// +// Created by Riley Testut on 10/28/16. +// Copyright © 2016 Riley Testut. All rights reserved. +// + +import UIKit + +import DeltaCore + +class ControllerSkinImageCacheKey: NSObject +{ + let controllerSkin: ControllerSkin + let traits: DeltaCore.ControllerSkin.Traits + let size: DeltaCore.ControllerSkin.Size + + override var hash: Int { + return self.controllerSkin.hashValue ^ self.traits.hashValue ^ self.size.hashValue + } + + init(controllerSkin: ControllerSkin, traits: DeltaCore.ControllerSkin.Traits, size: DeltaCore.ControllerSkin.Size) + { + self.controllerSkin = controllerSkin + self.traits = traits + self.size = size + + super.init() + } + + override func isEqual(_ object: Any?) -> Bool + { + guard let object = object as? ControllerSkinImageCacheKey else { return false } + return self.controllerSkin == object.controllerSkin && self.traits == object.traits && self.size == object.size + } +} + +class LoadControllerSkinImageOperation: LoadImageOperation +{ + let controllerSkin: ControllerSkin + let traits: DeltaCore.ControllerSkin.Traits + let size: DeltaCore.ControllerSkin.Size + + init(controllerSkin: ControllerSkin, traits: DeltaCore.ControllerSkin.Traits, size: DeltaCore.ControllerSkin.Size) + { + self.controllerSkin = controllerSkin + self.traits = traits + self.size = size + + let cacheKey = ControllerSkinImageCacheKey(controllerSkin: controllerSkin, traits: traits, size: size) + super.init(cacheKey: cacheKey) + } + + override func loadImage() -> UIImage? + { + let image = self.controllerSkin.image(for: self.traits, preferredSize: self.size) + return image + } +} diff --git a/Common/Components/LoadImageOperation.swift b/Common/Components/LoadImageOperation.swift index 2c58bc3..420342b 100644 --- a/Common/Components/LoadImageOperation.swift +++ b/Common/Components/LoadImageOperation.swift @@ -11,11 +11,9 @@ import ImageIO import Roxas -public class LoadImageOperation: RSTOperation +class LoadImageOperation: RSTOperation { - public let URL: Foundation.URL - - public var completionHandler: ((UIImage?) -> Void)? { + var completionHandler: ((UIImage?) -> Void)? { didSet { self.completionBlock = { rst_dispatch_sync_on_main_thread() { @@ -25,49 +23,47 @@ public class LoadImageOperation: RSTOperation } } - public var imageCache: NSCache? { + var imageCache: NSCache? { didSet { // Ensures if an image is cached, it will be returned immediately, to prevent temporary flash of placeholder image - self.isImmediate = self.imageCache?.object(forKey: self.URL as NSURL) != nil + self.isImmediate = self.imageCache?.object(forKey: self.cacheKey) != nil } } - fileprivate var image: UIImage? + private let cacheKey: CacheKeyType + private var image: UIImage? - public init(URL: Foundation.URL) + init(cacheKey: CacheKeyType) { - self.URL = URL + self.cacheKey = cacheKey super.init() } -} - -public extension LoadImageOperation -{ - override public func main() + + override func main() { guard !self.isCancelled else { return } - if let cachedImage = self.imageCache?.object(forKey: self.URL as NSURL) + if let cachedImage = self.imageCache?.object(forKey: self.cacheKey) { self.image = cachedImage return } - let options: NSDictionary = [kCGImageSourceShouldCache as NSString: true] + guard let loadedImage = self.loadImage() else { return } - if let imageSource = CGImageSourceCreateWithURL(self.URL as CFURL, options), let quartzImage = CGImageSourceCreateImageAtIndex(imageSource, 0, options) - { - let loadedImage = UIImage(cgImage: quartzImage) - - // Force decompression of image - UIGraphicsBeginImageContextWithOptions(CGSize(width: 1, height: 1), true, 1.0) - loadedImage.draw(at: CGPoint.zero) - UIGraphicsEndImageContext() - - self.imageCache?.setObject(loadedImage, forKey: self.URL as NSURL) - - self.image = loadedImage - } + // Force decompression of image + UIGraphicsBeginImageContextWithOptions(CGSize(width: 1, height: 1), true, 1.0) + loadedImage.draw(at: CGPoint.zero) + UIGraphicsEndImageContext() + + self.imageCache?.setObject(loadedImage, forKey: self.cacheKey) + + self.image = loadedImage + } + + func loadImage() -> UIImage? + { + return nil } } diff --git a/Common/Components/LoadImageURLOperation.swift b/Common/Components/LoadImageURLOperation.swift new file mode 100644 index 0000000..888a5ed --- /dev/null +++ b/Common/Components/LoadImageURLOperation.swift @@ -0,0 +1,33 @@ +// +// LoadImageURLOperation.swift +// Delta +// +// Created by Riley Testut on 10/28/16. +// Copyright © 2016 Riley Testut. All rights reserved. +// + +import UIKit +import ImageIO + +import Roxas + +class LoadImageURLOperation: LoadImageOperation +{ + public let url: URL + + init(url: URL) + { + self.url = url + super.init(cacheKey: url as NSURL) + } + + override func loadImage() -> UIImage? + { + let options: NSDictionary = [kCGImageSourceShouldCache as NSString: true] + + guard let imageSource = CGImageSourceCreateWithURL(self.url as CFURL, options), let quartzImage = CGImageSourceCreateImageAtIndex(imageSource, 0, options) else { return nil } + + let image = UIImage(cgImage: quartzImage) + return image + } +} diff --git a/Common/Database/Model/ControllerSkin.swift b/Common/Database/Model/ControllerSkin.swift index d936162..48ea7e0 100644 --- a/Common/Database/Model/ControllerSkin.swift +++ b/Common/Database/Model/ControllerSkin.swift @@ -65,6 +65,15 @@ public class ControllerSkin: _ControllerSkin let controllerSkin = self.isStandard ? DeltaCore.ControllerSkin.standardControllerSkin(for: self.gameType) : DeltaCore.ControllerSkin(fileURL: self.fileURL) return controllerSkin }() + + public override func awakeFromFetch() + { + super.awakeFromFetch() + + // Kinda hacky, but we initialize controllerSkin on fetch to ensure it is initialized on the correct thread + // We could solve this by wrapping controllerSkin.getter in performAndWait block, but this can lead to a deadlock + _ = self.controllerSkin + } } extension ControllerSkin: ControllerSkinProtocol diff --git a/Delta.xcodeproj/project.pbxproj b/Delta.xcodeproj/project.pbxproj index 6d72891..32276e6 100644 --- a/Delta.xcodeproj/project.pbxproj +++ b/Delta.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ BF0418141D01E93400E85BCF /* GBADeltaCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF0418131D01E93400E85BCF /* GBADeltaCore.framework */; }; BF0418151D01E93400E85BCF /* GBADeltaCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BF0418131D01E93400E85BCF /* GBADeltaCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + BF04E6FF1DB8625C000F35D3 /* ControllerSkinsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF04E6FE1DB8625C000F35D3 /* ControllerSkinsViewController.swift */; }; BF090CF41B490D8300DCAB45 /* UIDevice+Vibration.m in Sources */ = {isa = PBXBuildFile; fileRef = BF090CF31B490D8300DCAB45 /* UIDevice+Vibration.m */; }; BF0CDDAD1C8155D200640168 /* LoadImageOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0CDDAC1C8155D200640168 /* LoadImageOperation.swift */; }; BF107EC41BF413F000E0C32C /* GamesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF107EC31BF413F000E0C32C /* GamesViewController.swift */; }; @@ -39,6 +40,7 @@ BF3540001C5DA3C500C1184C /* PausePresentationControllerContentView.xib in Resources */ = {isa = PBXBuildFile; fileRef = BF353FFE1C5DA3C500C1184C /* PausePresentationControllerContentView.xib */; }; BF3540021C5DA3D500C1184C /* PauseStoryboardSegue.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3540011C5DA3D500C1184C /* PauseStoryboardSegue.swift */; }; BF3540081C5DAFAD00C1184C /* PauseTransitionCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3540071C5DAFAD00C1184C /* PauseTransitionCoordinator.swift */; }; + BF4F562B1DC3C6AF00267E32 /* LoadImageURLOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF4F562A1DC3C6AF00267E32 /* LoadImageURLOperation.swift */; }; BF5E7F441B9A650B00AE44F8 /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF5E7F431B9A650B00AE44F8 /* SettingsViewController.swift */; }; BF5E7F461B9A652600AE44F8 /* Settings.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BF5E7F451B9A652600AE44F8 /* Settings.storyboard */; }; BF6866171DCAC8B900BF2D06 /* ControllerSkin+Configuring.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6866161DCAC8B900BF2D06 /* ControllerSkin+Configuring.swift */; }; @@ -51,6 +53,7 @@ BF7AE80A1C2E8C7600B1B5BC /* UIColor+Delta.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF7AE8091C2E8C7600B1B5BC /* UIColor+Delta.swift */; }; BF7AE81E1C2E984300B1B5BC /* GridCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF7AE81A1C2E984300B1B5BC /* GridCollectionViewCell.swift */; }; BF7AE8241C2E984300B1B5BC /* GridCollectionViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF7AE81D1C2E984300B1B5BC /* GridCollectionViewLayout.swift */; }; + BF99A5971DC2F9C400468E9E /* ControllerSkinTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF99A5961DC2F9C400468E9E /* ControllerSkinTableViewCell.swift */; }; BF99C6941D0A9AA600BA92BC /* SNESDeltaCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BFC134E01AAD82460087AD7B /* SNESDeltaCore.framework */; }; BF99C6951D0A9AA600BA92BC /* SNESDeltaCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BFC134E01AAD82460087AD7B /* SNESDeltaCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; BF9F4FCF1AAD7B87004C9500 /* DeltaCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF9F4FCE1AAD7B87004C9500 /* DeltaCore.framework */; }; @@ -58,6 +61,7 @@ BFA0D1271D3AE1F600565894 /* GameViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF63BDE91D389EEB00FCB040 /* GameViewController.swift */; }; BFA2315C1CED10BE0011E35A /* Action.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA2315B1CED10BE0011E35A /* Action.swift */; }; BFAA1FED1B8AA4FA00495943 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFAA1FEC1B8AA4FA00495943 /* Settings.swift */; }; + BFACD5FD1DC3D2CE002D6DDC /* LoadControllerSkinImageOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF23C4941DC3CB2E00849B3E /* LoadControllerSkinImageOperation.swift */; }; BFBAA86A1D5A483900A29C1B /* DatabaseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFBAA8691D5A483900A29C1B /* DatabaseManager.swift */; }; BFC853351DB039AD00E8C372 /* _ControllerSkin.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFC853341DB039AD00E8C372 /* _ControllerSkin.swift */; }; BFC853371DB039B500E8C372 /* ControllerSkin.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFC853361DB039B500E8C372 /* ControllerSkin.swift */; }; @@ -106,6 +110,7 @@ BF02BCFE1D361BD1000892F2 /* NSFetchedResultsController+Conveniences.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSFetchedResultsController+Conveniences.h"; sourceTree = ""; }; BF02BCFF1D361BD1000892F2 /* NSFetchedResultsController+Conveniences.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSFetchedResultsController+Conveniences.m"; sourceTree = ""; }; BF0418131D01E93400E85BCF /* GBADeltaCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = GBADeltaCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + BF04E6FE1DB8625C000F35D3 /* ControllerSkinsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ControllerSkinsViewController.swift; path = "Controller Skins/ControllerSkinsViewController.swift"; sourceTree = ""; }; 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 = ""; }; @@ -118,6 +123,7 @@ BF13A7571D5D2FD9000BB055 /* EmulatorCore+Cheats.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "EmulatorCore+Cheats.swift"; sourceTree = ""; }; BF172AEA1C68986300C26774 /* NSManagedObjectContext+Conveniences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext+Conveniences.swift"; sourceTree = ""; }; BF1DAD5C1D9F576000E752A7 /* GameTypeControllerSkinsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GameTypeControllerSkinsViewController.swift; path = "Controller Skins/GameTypeControllerSkinsViewController.swift"; sourceTree = ""; }; + BF23C4941DC3CB2E00849B3E /* LoadControllerSkinImageOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = LoadControllerSkinImageOperation.swift; path = Components/LoadControllerSkinImageOperation.swift; sourceTree = ""; }; BF27CC861BC9E3C600A20D89 /* Delta.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = Delta.entitlements; sourceTree = ""; }; BF27CC8A1BC9FE4D00A20D89 /* Pods.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Pods.framework; path = "Pods/../build/Debug-appletvos/Pods.framework"; 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; }; @@ -142,6 +148,7 @@ BF3540011C5DA3D500C1184C /* PauseStoryboardSegue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PauseStoryboardSegue.swift; path = "Pause Menu/Segues/PauseStoryboardSegue.swift"; sourceTree = ""; }; BF3540041C5DA70400C1184C /* SaveStatesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SaveStatesViewController.swift; path = "Pause Menu/Save States/SaveStatesViewController.swift"; sourceTree = ""; }; BF3540071C5DAFAD00C1184C /* PauseTransitionCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PauseTransitionCoordinator.swift; path = "Pause Menu/Segues/PauseTransitionCoordinator.swift"; sourceTree = ""; }; + BF4F562A1DC3C6AF00267E32 /* LoadImageURLOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = LoadImageURLOperation.swift; path = Components/LoadImageURLOperation.swift; sourceTree = ""; }; BF5E7F431B9A650B00AE44F8 /* SettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; }; BF5E7F451B9A652600AE44F8 /* Settings.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Settings.storyboard; sourceTree = ""; }; BF63BDE91D389EEB00FCB040 /* GameViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GameViewController.swift; sourceTree = ""; }; @@ -155,6 +162,7 @@ BF7AE8091C2E8C7600B1B5BC /* UIColor+Delta.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Delta.swift"; sourceTree = ""; }; BF7AE81A1C2E984300B1B5BC /* GridCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GridCollectionViewCell.swift; path = "Collection View/GridCollectionViewCell.swift"; sourceTree = ""; }; BF7AE81D1C2E984300B1B5BC /* GridCollectionViewLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GridCollectionViewLayout.swift; path = "Collection View/GridCollectionViewLayout.swift"; sourceTree = ""; }; + BF99A5961DC2F9C400468E9E /* ControllerSkinTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ControllerSkinTableViewCell.swift; path = "Controller Skins/ControllerSkinTableViewCell.swift"; sourceTree = ""; }; BF9F4FCE1AAD7B87004C9500 /* DeltaCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = DeltaCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; BFA2315B1CED10BE0011E35A /* Action.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Action.swift; path = Components/Action.swift; sourceTree = ""; }; BFAA1FEC1B8AA4FA00495943 /* Settings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = ""; }; @@ -241,6 +249,8 @@ isa = PBXGroup; children = ( BF1DAD5C1D9F576000E752A7 /* GameTypeControllerSkinsViewController.swift */, + BF04E6FE1DB8625C000F35D3 /* ControllerSkinsViewController.swift */, + BF99A5961DC2F9C400468E9E /* ControllerSkinTableViewCell.swift */, ); name = "Controller Skins"; sourceTree = ""; @@ -441,6 +451,8 @@ children = ( BFA2315B1CED10BE0011E35A /* Action.swift */, BF0CDDAC1C8155D200640168 /* LoadImageOperation.swift */, + BF4F562A1DC3C6AF00267E32 /* LoadImageURLOperation.swift */, + BF23C4941DC3CB2E00849B3E /* LoadControllerSkinImageOperation.swift */, ); name = Components; sourceTree = ""; @@ -529,13 +541,13 @@ buildConfigurationList = BFFA71F61AAC406100EE9DD1 /* Build configuration list for PBXNativeTarget "Delta" */; buildPhases = ( A5AEDE02D1134013AEC5BCB6 /* Check Pods Manifest.lock */, + BF28980A1DAAFB0F0023D8E9 /* mogenerator */, BFFA71D31AAC406100EE9DD1 /* Sources */, BFFA71D41AAC406100EE9DD1 /* Frameworks */, BFFA71D51AAC406100EE9DD1 /* Resources */, BF9F4FCC1AAD7AEE004C9500 /* Embed Frameworks */, AA90DB8D72559A44728B98F0 /* Copy Pods Resources */, AEE0EDDF2E4AAD91E135FE80 /* Embed Pods Frameworks */, - BF28980A1DAAFB0F0023D8E9 /* mogenerator */, ); buildRules = ( ); @@ -684,6 +696,7 @@ BFE4269E1D9C68E600DC913F /* SaveStatesStoryboardSegue.swift in Sources */, BFFA71DD1AAC406100EE9DD1 /* AppDelegate.swift in Sources */, BF2898241DAAFD720023D8E9 /* _SaveState.swift in Sources */, + BF04E6FF1DB8625C000F35D3 /* ControllerSkinsViewController.swift in Sources */, BF7AE81E1C2E984300B1B5BC /* GridCollectionViewCell.swift in Sources */, BF34FA111CF1899D006624C7 /* CheatTextView.swift in Sources */, BF1DAD5D1D9F576000E752A7 /* GameTypeControllerSkinsViewController.swift in Sources */, @@ -700,6 +713,7 @@ BFFC464C1D5998D600AF2CC6 /* CheatTableViewCell.swift in Sources */, BF7AE80A1C2E8C7600B1B5BC /* UIColor+Delta.swift in Sources */, BF090CF41B490D8300DCAB45 /* UIDevice+Vibration.m in Sources */, + BFACD5FD1DC3D2CE002D6DDC /* LoadControllerSkinImageOperation.swift in Sources */, BF797A2D1C2D339F00F1A000 /* UILabel+FontSize.swift in Sources */, BFD097211D3A01B8005A44C2 /* SaveStatesViewController.swift in Sources */, BF3540021C5DA3D500C1184C /* PauseStoryboardSegue.swift in Sources */, @@ -717,7 +731,9 @@ BF2898161DAAFC2A0023D8E9 /* Game.swift in Sources */, BF1173501DA32CF600047DF8 /* ControllersSettingsViewController.swift in Sources */, BFFC461E1D59823500AF2CC6 /* GamesPresentationController.swift in Sources */, + BF99A5971DC2F9C400468E9E /* ControllerSkinTableViewCell.swift in Sources */, BF5E7F441B9A650B00AE44F8 /* SettingsViewController.swift in Sources */, + BF4F562B1DC3C6AF00267E32 /* LoadImageURLOperation.swift in Sources */, BF2898231DAAFD720023D8E9 /* _GameCollection.swift in Sources */, BF353FF21C5D7FB000C1184C /* PauseViewController.swift in Sources */, BFDB28451BC9DA7B001D0C83 /* ImportController.swift in Sources */, diff --git a/Delta/Pause Menu/Save States/SaveStatesViewController.swift b/Delta/Pause Menu/Save States/SaveStatesViewController.swift index 348bf76..98815f2 100644 --- a/Delta/Pause Menu/Save States/SaveStatesViewController.swift +++ b/Delta/Pause Menu/Save States/SaveStatesViewController.swift @@ -242,7 +242,7 @@ private extension SaveStatesViewController if !ignoreOperations { - let imageOperation = LoadImageOperation(URL: saveState.imageFileURL) + let imageOperation = LoadImageURLOperation(url: saveState.imageFileURL) imageOperation.imageCache = self.imageCache imageOperation.completionHandler = { image in diff --git a/Delta/Settings/Controller Skins/ControllerSkinTableViewCell.swift b/Delta/Settings/Controller Skins/ControllerSkinTableViewCell.swift new file mode 100644 index 0000000..71bf0dc --- /dev/null +++ b/Delta/Settings/Controller Skins/ControllerSkinTableViewCell.swift @@ -0,0 +1,26 @@ +// +// ControllerSkinTableViewCell.swift +// Delta +// +// Created by Riley Testut on 10/27/16. +// Copyright © 2016 Riley Testut. All rights reserved. +// + +import UIKit + +class ControllerSkinTableViewCell: UITableViewCell +{ + @IBOutlet var controllerSkinImageView: UIImageView! + @IBOutlet var activityIndicatorView: UIActivityIndicatorView! + + override func awakeFromNib() + { + super.awakeFromNib() + } + + override func setSelected(_ selected: Bool, animated: Bool) + { + super.setSelected(selected, animated: animated) + } + +} diff --git a/Delta/Settings/Controller Skins/ControllerSkinsViewController.swift b/Delta/Settings/Controller Skins/ControllerSkinsViewController.swift new file mode 100644 index 0000000..75a0ab6 --- /dev/null +++ b/Delta/Settings/Controller Skins/ControllerSkinsViewController.swift @@ -0,0 +1,199 @@ +// +// ControllerSkinsViewController.swift +// Delta +// +// Created by Riley Testut on 10/19/16. +// Copyright © 2016 Riley Testut. All rights reserved. +// + +import UIKit + +import DeltaCore + +import Roxas + +extension ControllerSkinsViewController +{ + enum Section: Int + { + case standard + case custom + } +} + +class ControllerSkinsViewController: UITableViewController +{ + var gameType: GameType! { + didSet { + self.updateDataSource() + } + } + + var traits: DeltaCore.ControllerSkin.Traits! { + didSet { + self.updateDataSource() + } + } + + fileprivate var dataSource: RSTFetchedResultsTableViewDataSource! + + fileprivate let imageOperationQueue = RSTOperationQueue() + + fileprivate let imageCache = NSCache() +} + +extension ControllerSkinsViewController +{ + override func viewDidLoad() + { + super.viewDidLoad() + } + + override func viewWillAppear(_ animated: Bool) + { + self.dataSource.fetchedResultsController.performFetchIfNeeded() + + super.viewWillAppear(animated) + } + + override func didReceiveMemoryWarning() + { + super.didReceiveMemoryWarning() + // Dispose of any resources that can be recreated. + } +} + +private extension ControllerSkinsViewController +{ + //MARK: - Update + func updateDataSource() + { + guard let gameType = self.gameType, let traits = self.traits else { return } + + let configuration = ControllerSkinConfigurations(traits: traits) + + let fetchRequest: NSFetchRequest = ControllerSkin.fetchRequest() + fetchRequest.predicate = NSPredicate(format: "%K == %@ AND (%K & %d) == %d", #keyPath(ControllerSkin.gameType), gameType.rawValue, #keyPath(ControllerSkin.supportedConfigurations), configuration.rawValue, configuration.rawValue) + fetchRequest.sortDescriptors = [NSSortDescriptor(key: #keyPath(ControllerSkin.isStandard), ascending: false), NSSortDescriptor(key: #keyPath(ControllerSkin.name), ascending: true)] + + let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext, sectionNameKeyPath: #keyPath(ControllerSkin.name), cacheName: nil) + + self.dataSource = RSTFetchedResultsTableViewDataSource(fetchedResultsController: fetchedResultsController) + self.dataSource.cellIdentifierHandler = { _ in RSTGenericCellIdentifier } + self.dataSource.cellConfigurationHandler = { [unowned self] (cell, indexPath) in + self.configure(cell as! ControllerSkinTableViewCell, for: indexPath) + } + } + + //MARK: - Configure Cells + func configure(_ cell: ControllerSkinTableViewCell, for indexPath: IndexPath) + { + let controllerSkin = self.dataSource.fetchedResultsController.object(at: indexPath) + + cell.controllerSkinImageView.image = nil + cell.activityIndicatorView.startAnimating() + + let size = UIScreen.main.defaultControllerSkinSize + + let imageOperation = LoadControllerSkinImageOperation(controllerSkin: controllerSkin, traits: self.traits, size: size) + imageOperation.imageCache = self.imageCache + imageOperation.completionHandler = { image in + + guard let image = image else { return } + + if !imageOperation.isImmediate + { + UIView.transition(with: cell.controllerSkinImageView, duration: 0.2, options: .transitionCrossDissolve, animations: { + cell.controllerSkinImageView.image = image + }, completion: nil) + } + else + { + cell.controllerSkinImageView.image = image + } + + cell.activityIndicatorView.stopAnimating() + } + + // Ensure initially visible cells have loaded their image before they appear to prevent potential flickering from placeholder to thumbnail + if self.isAppearing + { + imageOperation.isImmediate = true + } + + self.imageOperationQueue.addOperation(imageOperation, forKey: indexPath as NSCopying) + } +} + +extension ControllerSkinsViewController +{ + override func numberOfSections(in tableView: UITableView) -> Int + { + return self.dataSource.numberOfSections(in: tableView) + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int + { + return self.dataSource.tableView(tableView, numberOfRowsInSection: section) + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell + { + return self.dataSource.tableView(tableView, cellForRowAt: indexPath) + } + + override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? + { + let controllerSkin = self.dataSource.fetchedResultsController.object(at: IndexPath(row: 0, section: section)) + return controllerSkin.name + } +} + +extension ControllerSkinsViewController: UITableViewDataSourcePrefetching +{ + func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) + { + for indexPath in indexPaths + { + let controllerSkin = self.dataSource.fetchedResultsController.object(at: indexPath) + + let size = UIScreen.main.defaultControllerSkinSize + + let imageOperation = LoadControllerSkinImageOperation(controllerSkin: controllerSkin, traits: self.traits, size: size) + imageOperation.imageCache = self.imageCache + + self.imageOperationQueue.addOperation(imageOperation, forKey: indexPath as NSCopying) + } + } + + func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) + { + for indexPath in indexPaths + { + let operation = self.imageOperationQueue[indexPath as NSCopying] + operation?.cancel() + } + } +} + +extension ControllerSkinsViewController +{ + override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat + { + let controllerSkin = self.dataSource.fetchedResultsController.object(at: indexPath) + + guard let size = controllerSkin.aspectRatio(for: self.traits) else { return 150 } + + let scale = (self.view.bounds.width / size.width) + + let height = size.height * scale + + return height + } + + override func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) + { + let operation = self.imageOperationQueue[indexPath as NSCopying] + operation?.cancel() + } +} diff --git a/Delta/Settings/Controller Skins/GameTypeControllerSkinsViewController.swift b/Delta/Settings/Controller Skins/GameTypeControllerSkinsViewController.swift index 4ca3a91..591055d 100644 --- a/Delta/Settings/Controller Skins/GameTypeControllerSkinsViewController.swift +++ b/Delta/Settings/Controller Skins/GameTypeControllerSkinsViewController.swift @@ -47,6 +47,25 @@ extension GameTypeControllerSkinsViewController { super.didReceiveMemoryWarning() } + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) + { + guard let cell = sender as? UITableViewCell, let indexPath = self.tableView.indexPath(for: cell) else { return } + + let controllerSkinsViewController = segue.destination as! ControllerSkinsViewController + controllerSkinsViewController.gameType = self.gameType + + var traits = DeltaCore.ControllerSkin.Traits.defaults(for: self.view) + + let section = Section(rawValue: indexPath.section)! + switch section + { + case .portrait: traits.orientation = .portrait + case .landscape: traits.orientation = .landscape + } + + controllerSkinsViewController.traits = traits + } } extension GameTypeControllerSkinsViewController @@ -70,6 +89,13 @@ extension GameTypeControllerSkinsViewController let height = unwrappedImageSize.height * scale return height } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) + { + guard let cell = tableView.cellForRow(at: indexPath) else { return } + + self.performSegue(withIdentifier: "showControllerSkins", sender: cell) + } } private extension GameTypeControllerSkinsViewController diff --git a/Delta/Settings/Settings.storyboard b/Delta/Settings/Settings.storyboard index b64d9e4..7f4993b 100644 --- a/Delta/Settings/Settings.storyboard +++ b/Delta/Settings/Settings.storyboard @@ -1,8 +1,11 @@ - + + + + - + @@ -18,21 +21,21 @@ - + - + - + - + - + - + - + - +