From 925fb9454d96da1be2a3d84f8cdb4cfaa0bf284a Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Tue, 24 Nov 2015 11:53:36 -0800 Subject: [PATCH] Updated UI for pagination between GameCollections --- .../GameCollectionViewDataSource.swift | 17 +- Common/Database/DatabaseManager.swift | 54 +++++- Common/Database/Model/Game.swift | 14 +- .../GameCollection+CoreDataProperties.swift | 6 +- Common/Database/Model/GameCollection.swift | 48 +++-- .../Model.xcdatamodel/contents | 9 +- Common/Importing/GamePickerController.swift | 4 +- Delta.xcodeproj/project.pbxproj | 16 +- .../xcshareddata/Delta.xcscmblueprint | 8 + Delta/Base.lproj/Main.storyboard | 10 +- .../GamesCollectionViewController.swift | 5 +- .../Game Selection/GamesViewController.swift | 174 ++++++++++++++++-- DeltaTV/GameSelectionViewController.swift | 31 +++- External/Roxas | 2 +- 14 files changed, 323 insertions(+), 75 deletions(-) diff --git a/Common/Collection View/GameCollectionViewDataSource.swift b/Common/Collection View/GameCollectionViewDataSource.swift index 0e20705..84deac4 100644 --- a/Common/Collection View/GameCollectionViewDataSource.swift +++ b/Common/Collection View/GameCollectionViewDataSource.swift @@ -11,15 +11,13 @@ import CoreData class GameCollectionViewDataSource: NSObject { - var gameTypeIdentifiers: [String] = [] { + var supportedGameCollectionIdentifiers: [String]? { didSet { self.updateFetchedResultsController() } } - var sectionTitles: [String] = [] - var cellConfigurationHandler: ((GameCollectionViewCell, Game) -> Void)? private(set) var fetchedResultsController: NSFetchedResultsController = NSFetchedResultsController() @@ -47,10 +45,13 @@ class GameCollectionViewDataSource: NSObject var predicates: [NSPredicate] = [] - for typeIdentifier in self.gameTypeIdentifiers + if let identifiers = self.supportedGameCollectionIdentifiers { - let predicate = NSPredicate(format: "%K == %@", GameAttributes.typeIdentifier.rawValue, typeIdentifier) - predicates.append(predicate) + for identifier in identifiers + { + let predicate = NSPredicate(format: "SUBQUERY(%K, $x, $x.%K == %@).@count > 0", GameAttributes.gameCollections.rawValue, GameCollectionAttributes.identifier.rawValue, identifier) + predicates.append(predicate) + } } if predicates.count > 0 @@ -58,7 +59,7 @@ class GameCollectionViewDataSource: NSObject fetchRequest.predicate = NSCompoundPredicate(orPredicateWithSubpredicates: predicates) } - fetchRequest.sortDescriptors = [NSSortDescriptor(key: GameAttributes.name.rawValue, ascending: true)] + fetchRequest.sortDescriptors = [NSSortDescriptor(key: GameAttributes.typeIdentifier.rawValue, ascending: true), NSSortDescriptor(key: GameAttributes.name.rawValue, ascending: true)] self.fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.sharedManager.managedObjectContext, sectionNameKeyPath: GameAttributes.typeIdentifier.rawValue, cacheName: nil) self.fetchedResultsController.delegate = previousDelegate @@ -117,6 +118,4 @@ extension GameCollectionViewDataSource: UICollectionViewDelegate let size = self.prototypeCell.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize) return size } - - } diff --git a/Common/Database/DatabaseManager.swift b/Common/Database/DatabaseManager.swift index 1d0ccab..5285829 100644 --- a/Common/Database/DatabaseManager.swift +++ b/Common/Database/DatabaseManager.swift @@ -88,8 +88,9 @@ class DatabaseManager self.validationManagedObjectContext = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType) self.validationManagedObjectContext.parentContext = self.managedObjectContext self.validationManagedObjectContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy - + NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("managedObjectContextDidSave:"), name: NSManagedObjectContextDidSaveNotification, object: nil) + } func startWithCompletion(completionBlock: ((performingMigration: Bool) -> Void)?) @@ -158,9 +159,9 @@ class DatabaseManager game.identifier = identifier game.filename = filename - if let pathExtension = URL.pathExtension, - gameCollection = GameCollection.gameSystemCollectionForPathExtension(pathExtension, inManagedObjectContext: managedObjectContext) + if let pathExtension = URL.pathExtension { + let gameCollection = GameCollection.gameSystemCollectionForPathExtension(pathExtension, inManagedObjectContext: managedObjectContext) game.typeIdentifier = gameCollection.identifier game.gameCollections.insert(gameCollection) } @@ -171,7 +172,19 @@ class DatabaseManager do { - try NSFileManager.defaultManager().moveItemAtURL(URL, toURL: DatabaseManager.gamesDirectoryURL.URLByAppendingPathComponent(game.identifier + ".smc")) + let destinationURL = DatabaseManager.gamesDirectoryURL.URLByAppendingPathComponent(game.identifier + "." + game.preferredFileExtension) + + if let path = destinationURL.path + { + if NSFileManager.defaultManager().fileExistsAtPath(path) + { + try NSFileManager.defaultManager().removeItemAtURL(URL) + } + else + { + try NSFileManager.defaultManager().moveItemAtURL(URL, toURL: destinationURL) + } + } identifiers.append(game.identifier) } @@ -275,7 +288,36 @@ private extension DatabaseManager { guard let managedObjectContext = notification.object as? NSManagedObjectContext where managedObjectContext.parentContext == self.validationManagedObjectContext else { return } - self.save() + self.validationManagedObjectContext.performBlockAndWait { + + // Remove deleted games from disk + if let deletedObjects = notification.userInfo?[NSDeletedObjectsKey] as? Set + { + let games = deletedObjects.filter({ $0 is Game }).map({ self.validationManagedObjectContext.objectWithID($0.objectID) as! Game }) + + for game in games + { + do + { + try NSFileManager.defaultManager().removeItemAtURL(game.fileURL) + } + catch let error as NSError + { + print(error) + } + } + } + + // Remove empty collections + let collections = GameCollection.instancesWithPredicate(NSPredicate(format: "%K.@count == 0", GameCollectionAttributes.games.rawValue), inManagedObjectContext: self.validationManagedObjectContext, type: GameCollection.self) + + for collection in collections + { + self.validationManagedObjectContext.deleteObject(collection) + } + + self.save() + } } - + } diff --git a/Common/Database/Model/Game.swift b/Common/Database/Model/Game.swift index 7c6b909..e12873d 100644 --- a/Common/Database/Model/Game.swift +++ b/Common/Database/Model/Game.swift @@ -12,6 +12,8 @@ import CoreData import DeltaCore import SNESDeltaCore +public let kUTTypeGBAGame: CFStringRef = "com.rileytestut.delta.game.gba" + @objc(Game) class Game: NSManagedObject, GameType { @@ -19,12 +21,22 @@ class Game: NSManagedObject, GameType let fileURL = DatabaseManager.gamesDirectoryURL.URLByAppendingPathComponent(self.filename) return fileURL } + + var preferredFileExtension: String { + switch self.typeIdentifier + { + case kUTTypeSNESGame as String as String: return "smc" + case kUTTypeGBAGame as String as String: return "gba" + case kUTTypeDeltaGame as String as String: fallthrough + default: return "delta" + } + } } extension Game { class func supportedTypeIdentifiers() -> Set { - return [kUTTypeSNESGame as String] + return [kUTTypeSNESGame as String, kUTTypeGBAGame as String] } } diff --git a/Common/Database/Model/GameCollection+CoreDataProperties.swift b/Common/Database/Model/GameCollection+CoreDataProperties.swift index 8d0b791..2a51c99 100644 --- a/Common/Database/Model/GameCollection+CoreDataProperties.swift +++ b/Common/Database/Model/GameCollection+CoreDataProperties.swift @@ -14,18 +14,16 @@ import CoreData enum GameCollectionAttributes: String { - case name case identifier - case shortName + case index case games } extension GameCollection { - @NSManaged var name: String @NSManaged var identifier: String - @NSManaged var shortName: String? + @NSManaged var index: Int16 @NSManaged var games: Set } diff --git a/Common/Database/Model/GameCollection.swift b/Common/Database/Model/GameCollection.swift index caa6cec..19c1703 100644 --- a/Common/Database/Model/GameCollection.swift +++ b/Common/Database/Model/GameCollection.swift @@ -9,31 +9,54 @@ import Foundation import CoreData +import DeltaCore import SNESDeltaCore @objc(GameCollection) class GameCollection: NSManagedObject { - class func gameSystemCollectionForPathExtension(pathExtension: String?, inManagedObjectContext managedObjectContext: NSManagedObjectContext) -> GameCollection? + var name: String { + + switch self.identifier + { + case kUTTypeSNESGame as String as String: return NSLocalizedString("Super Nintendo Entertainment System", comment: "") + case kUTTypeGBAGame as String as String: return NSLocalizedString("Game Boy Advance", comment: "") + case kUTTypeDeltaGame as String as String: return NSLocalizedString("Unsupported Games", comment: "") + default: return NSLocalizedString("Unknown", comment: "") + } + } + + var shortName: String { + + switch self.identifier + { + case kUTTypeSNESGame as String as String: return NSLocalizedString("SNES", comment: "") + case kUTTypeGBAGame as String as String: return NSLocalizedString("GBA", comment: "") + case kUTTypeDeltaGame as String as String: return NSLocalizedString("Unsupported", comment: "") + default: return NSLocalizedString("Unknown", comment: "") + } + } + + class func gameSystemCollectionForPathExtension(pathExtension: String?, inManagedObjectContext managedObjectContext: NSManagedObjectContext) -> GameCollection { - guard let pathExtension = pathExtension else { return nil } - let identifier: String - let name: String - let shortName: String + let index: Int16 - switch pathExtension + switch pathExtension ?? "" { case "smc": fallthrough case "sfc": fallthrough case "fig": identifier = kUTTypeSNESGame as String - name = "Super Nintendo Entertainment System" - shortName = "SNES" + index = 1990 + case "gba": + identifier = kUTTypeGBAGame as String + index = 2001 - - default: return nil + default: + identifier = kUTTypeDeltaGame as String + index = Int16(INT16_MAX) } let predicate = NSPredicate(format: "%K == %@", GameCollectionAttributes.identifier.rawValue, identifier) @@ -43,10 +66,9 @@ class GameCollection: NSManagedObject { gameCollection = GameCollection.insertIntoManagedObjectContext(managedObjectContext) gameCollection?.identifier = identifier - gameCollection?.name = name - gameCollection?.shortName = shortName + gameCollection?.index = index } - return gameCollection + return gameCollection! } } diff --git a/Common/Database/Model/Model.xcdatamodeld/Model.xcdatamodel/contents b/Common/Database/Model/Model.xcdatamodeld/Model.xcdatamodel/contents index 803fdd2..4a05fa0 100644 --- a/Common/Database/Model/Model.xcdatamodeld/Model.xcdatamodel/contents +++ b/Common/Database/Model/Model.xcdatamodeld/Model.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -23,8 +23,7 @@ - - + @@ -33,7 +32,7 @@ - - + + \ No newline at end of file diff --git a/Common/Importing/GamePickerController.swift b/Common/Importing/GamePickerController.swift index 607306f..004a603 100644 --- a/Common/Importing/GamePickerController.swift +++ b/Common/Importing/GamePickerController.swift @@ -9,6 +9,8 @@ import UIKit import ObjectiveC +import DeltaCore + protocol GamePickerControllerDelegate { func gamePickerController(gamePickerController: GamePickerController, didImportGames games: [Game]) @@ -59,7 +61,7 @@ class GamePickerController: NSObject let managedObjectContext = DatabaseManager.sharedManager.backgroundManagedObjectContext() managedObjectContext.performBlock() { - let gameURLs = contents.filter({ GameCollection.gameSystemCollectionForPathExtension($0.pathExtension, inManagedObjectContext: managedObjectContext) != nil }) + let gameURLs = contents.filter({ GameCollection.gameSystemCollectionForPathExtension($0.pathExtension, inManagedObjectContext: managedObjectContext).identifier != kUTTypeDeltaGame as String }) self.importGamesAtURLs(gameURLs) } diff --git a/Delta.xcodeproj/project.pbxproj b/Delta.xcodeproj/project.pbxproj index d5a1d6e..086a9b2 100644 --- a/Delta.xcodeproj/project.pbxproj +++ b/Delta.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* 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 */; }; + BF107EC41BF413F000E0C32C /* GamesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF107EC31BF413F000E0C32C /* GamesViewController.swift */; }; BF27CC8B1BC9FE4D00A20D89 /* Pods.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF27CC8A1BC9FE4D00A20D89 /* Pods.framework */; }; BF27CC8C1BC9FE5300A20D89 /* Pods.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 22506DA00971C4300AF90A35 /* Pods.framework */; }; BF27CC8D1BC9FE5300A20D89 /* Pods.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 22506DA00971C4300AF90A35 /* Pods.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -17,7 +18,6 @@ BF27CC911BCB156200A20D89 /* EmulationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF27CC901BCB156200A20D89 /* EmulationViewController.swift */; }; BF27CC951BCB7B7A00A20D89 /* GameController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF27CC941BCB7B7A00A20D89 /* GameController.framework */; }; BF27CC971BCC890700A20D89 /* GamesCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF27CC961BCC890700A20D89 /* GamesCollectionViewController.swift */; }; - BF27CC991BCC8AE600A20D89 /* GamesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF27CC981BCC8AE600A20D89 /* GamesViewController.swift */; }; BF2A53FB1BB74FC10052BD0C /* SNESDeltaCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BFC134E01AAD82460087AD7B /* SNESDeltaCore.framework */; }; 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 */; }; @@ -112,12 +112,12 @@ 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 = ""; }; + BF107EC31BF413F000E0C32C /* GamesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GamesViewController.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 = ""; }; 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 = ""; }; - BF27CC981BCC8AE600A20D89 /* GamesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GamesViewController.swift; sourceTree = ""; }; BF4566E71BC090B6007BFA1A /* Model.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model.xcdatamodel; sourceTree = ""; }; BF50DC411BD851740024C720 /* GameCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GameCollectionViewCell.swift; path = "Collection View/GameCollectionViewCell.swift"; sourceTree = ""; }; BF5E7F431B9A650B00AE44F8 /* SettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; }; @@ -204,7 +204,6 @@ BF4566E31BC09026007BFA1A /* Common */ = { isa = PBXGroup; children = ( - BFC273151BE612CE00D22B05 /* Model */, BFF1E55F1BE04BF6000E9EF6 /* Components */, BF9257571BD8244800B109DA /* Collection View */, BF4566E41BC0902E007BFA1A /* Database */, @@ -238,7 +237,7 @@ BF46894D1AAC469800A2586D /* Game Selection */ = { isa = PBXGroup; children = ( - BF27CC981BCC8AE600A20D89 /* GamesViewController.swift */, + BF107EC31BF413F000E0C32C /* GamesViewController.swift */, BF27CC961BCC890700A20D89 /* GamesCollectionViewController.swift */, ); path = "Game Selection"; @@ -308,13 +307,6 @@ path = Settings; sourceTree = ""; }; - BFC273151BE612CE00D22B05 /* Model */ = { - isa = PBXGroup; - children = ( - ); - name = Model; - sourceTree = ""; - }; BFDB28431BC9D9D1001D0C83 /* Importing */ = { isa = PBXGroup; children = ( @@ -632,7 +624,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - BF27CC991BCC8AE600A20D89 /* GamesViewController.swift in Sources */, BFF4EA011BE1B2420056AAA4 /* GameCollectionViewLayoutAttributes.swift in Sources */, BFAA1FED1B8AA4FA00495943 /* Settings.swift in Sources */, BF50DC421BD851740024C720 /* GameCollectionViewCell.swift in Sources */, @@ -650,6 +641,7 @@ BFA5342A1BDC6B520088F1BE /* GameCollectionViewLayout.swift in Sources */, BF762E9E1BC19D31002C8866 /* DatabaseManager.swift in Sources */, BF090CF41B490D8300DCAB45 /* UIDevice+Vibration.m in Sources */, + BF107EC41BF413F000E0C32C /* GamesViewController.swift in Sources */, BF5E7F441B9A650B00AE44F8 /* SettingsViewController.swift in Sources */, BFDB28451BC9DA7B001D0C83 /* GamePickerController.swift in Sources */, BFDE393A1BC0CEDF003F72E8 /* Game+CoreDataProperties.swift in Sources */, diff --git a/Delta.xcworkspace/xcshareddata/Delta.xcscmblueprint b/Delta.xcworkspace/xcshareddata/Delta.xcscmblueprint index bfaecf0..2252b11 100644 --- a/Delta.xcworkspace/xcshareddata/Delta.xcscmblueprint +++ b/Delta.xcworkspace/xcshareddata/Delta.xcscmblueprint @@ -1,4 +1,5 @@ { + "DVTSourceControlWorkspaceBlueprintPrimaryRemoteRepositoryKey" : "A1BF268530E217C860B34A81A1DA1B446006BF9E", "DVTSourceControlWorkspaceBlueprintWorkingCopyRepositoryLocationsKey" : { }, @@ -6,6 +7,7 @@ "377491A212AA92C1503AD101C88ADDBD6FD62CF0" : 0, "C14902E1A817B9BD0549E16C5F6A56E2377FBF42" : 0, "816D57F18EE93972A535858CB18F29A85BE91A39" : 0, + "A1BF268530E217C860B34A81A1DA1B446006BF9E" : 0, "82DF43EF5CA016D15C41E0AF1AF686E9973475B7" : 0 }, "DVTSourceControlWorkspaceBlueprintIdentifierKey" : "8EE86D07-2AAF-47C3-91DF-0E46DBDAB128", @@ -13,6 +15,7 @@ "377491A212AA92C1503AD101C88ADDBD6FD62CF0" : "Delta\/External\/Roxas\/", "C14902E1A817B9BD0549E16C5F6A56E2377FBF42" : "Delta\/Cores\/SNESDeltaCore\/", "816D57F18EE93972A535858CB18F29A85BE91A39" : "Delta\/Cores\/DeltaCore\/", + "A1BF268530E217C860B34A81A1DA1B446006BF9E" : "Delta\/", "82DF43EF5CA016D15C41E0AF1AF686E9973475B7" : "Delta\/Cores\/DeltaCore\/External\/ZipZap\/" }, "DVTSourceControlWorkspaceBlueprintNameKey" : "Delta", @@ -34,6 +37,11 @@ "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "82DF43EF5CA016D15C41E0AF1AF686E9973475B7" }, + { + "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "github.com:rileytestut\/Delta.git", + "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", + "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "A1BF268530E217C860B34A81A1DA1B446006BF9E" + }, { "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "github.com:rileytestut\/SNESDeltaCore.git", "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", diff --git a/Delta/Base.lproj/Main.storyboard b/Delta/Base.lproj/Main.storyboard index 43b9065..006350f 100644 --- a/Delta/Base.lproj/Main.storyboard +++ b/Delta/Base.lproj/Main.storyboard @@ -1,5 +1,5 @@ - + @@ -79,7 +79,7 @@ - + @@ -239,12 +239,18 @@ + + + + + + diff --git a/Delta/Game Selection/GamesCollectionViewController.swift b/Delta/Game Selection/GamesCollectionViewController.swift index ca620a6..63b9515 100644 --- a/Delta/Game Selection/GamesCollectionViewController.swift +++ b/Delta/Game Selection/GamesCollectionViewController.swift @@ -13,10 +13,11 @@ import DeltaCore class GamesCollectionViewController: UICollectionViewController { - var gameTypeIdentifier: String! { + var gameCollection: GameCollection! { didSet { - self.dataSource.gameTypeIdentifiers = [self.gameTypeIdentifier] + self.dataSource.supportedGameCollectionIdentifiers = [self.gameCollection.identifier] + self.title = self.gameCollection.shortName } } diff --git a/Delta/Game Selection/GamesViewController.swift b/Delta/Game Selection/GamesViewController.swift index a06f948..1840364 100644 --- a/Delta/Game Selection/GamesViewController.swift +++ b/Delta/Game Selection/GamesViewController.swift @@ -7,37 +7,95 @@ // import UIKit +import CoreData import SNESDeltaCore +import Roxas + class GamesViewController: UIViewController { - var pageViewController: UIPageViewController! = nil + private var pageViewController: UIPageViewController! + private var backgroundView: RSTBackgroundView! + private var pageControl: UIPageControl! + + private let fetchedResultsController: NSFetchedResultsController + + override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?) { + fatalError("initWithNibName: not implemented") + } + + required init?(coder aDecoder: NSCoder) + { + let fetchRequest = GameCollection.fetchRequest() + fetchRequest.sortDescriptors = [NSSortDescriptor(key: GameCollectionAttributes.index.rawValue, ascending: true)] + + self.fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.sharedManager.managedObjectContext, sectionNameKeyPath: nil, cacheName: nil) + + super.init(coder: aDecoder) + + self.fetchedResultsController.delegate = self + } - let supportedGameTypeIdentifiers = [kUTTypeSNESGame as String] - override func viewDidLoad() { super.viewDidLoad() self.automaticallyAdjustsScrollViewInsets = false + self.backgroundView = RSTBackgroundView(frame: self.view.bounds) + self.backgroundView.autoresizingMask = [.FlexibleWidth, .FlexibleHeight] + self.backgroundView.textLabel.text = NSLocalizedString("No Games", comment: "") + self.backgroundView.detailTextLabel.text = NSLocalizedString("You can import games by pressing the + button in the top right.", comment: "") + self.view.insertSubview(self.backgroundView, atIndex: 0) + self.pageViewController = self.childViewControllers.first as? UIPageViewController self.pageViewController.dataSource = self self.pageViewController.delegate = self + self.pageViewController.view.hidden = true - let viewController = self.viewControllerForIndex(0) - self.pageViewController.setViewControllers([viewController], direction: .Forward, animated: false, completion: nil) - - // Do any additional setup after loading the view. + self.pageControl = UIPageControl() + self.pageControl.translatesAutoresizingMaskIntoConstraints = false + self.pageControl.hidesForSinglePage = false + self.pageControl.numberOfPages = 3 + self.pageControl.currentPageIndicatorTintColor = UIColor.purpleColor() + self.pageControl.pageIndicatorTintColor = UIColor.lightGrayColor() + self.navigationController?.toolbar.addSubview(self.pageControl) + + self.pageControl.centerXAnchor.constraintEqualToAnchor(self.navigationController?.toolbar.centerXAnchor, constant: 0).active = true + self.pageControl.centerYAnchor.constraintEqualToAnchor(self.navigationController?.toolbar.centerYAnchor, constant: 0).active = true + } + + override func viewWillAppear(animated: Bool) + { + super.viewWillAppear(animated) + + if self.fetchedResultsController.fetchedObjects == nil + { + do + { + try self.fetchedResultsController.performFetch() + } + catch let error as NSError + { + print(error) + } + + self.updateSections() + } } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() - let viewController = self.pageViewController.viewControllers?.first as! GamesCollectionViewController - viewController.collectionView?.contentInset.top = self.topLayoutGuide.length + if let viewControllers = self.pageViewController.viewControllers as? [GamesCollectionViewController] + { + for viewController in viewControllers + { + viewController.collectionView?.contentInset.top = self.topLayoutGuide.length + } + } } override func didReceiveMemoryWarning() { @@ -57,20 +115,83 @@ class GamesViewController: UIViewController private extension GamesViewController { - func viewControllerForIndex(index: Int) -> GamesCollectionViewController + func viewControllerForIndex(index: Int) -> GamesCollectionViewController? { - var safeIndex = index % self.supportedGameTypeIdentifiers.count + guard let pages = self.fetchedResultsController.sections?.first?.numberOfObjects where pages > 0 else { return nil } + + // Return nil if only one section, and not asking for the 0th view controller + guard !(pages == 1 && index != 0) else { return nil } + + var safeIndex = index % pages if safeIndex < 0 { - safeIndex = self.supportedGameTypeIdentifiers.count + safeIndex + safeIndex = pages + safeIndex } - + + let indexPath = NSIndexPath(forRow: safeIndex, inSection: 0) + let viewController = self.storyboard?.instantiateViewControllerWithIdentifier("gamesCollectionViewController") as! GamesCollectionViewController - viewController.gameTypeIdentifier = self.supportedGameTypeIdentifiers[safeIndex] as String + viewController.gameCollection = self.fetchedResultsController.objectAtIndexPath(indexPath) as! GameCollection viewController.collectionView?.contentInset.top = self.topLayoutGuide.length return viewController } + + func updateSections() + { + let sections = self.fetchedResultsController.sections?.first?.numberOfObjects ?? 0 + self.pageControl.numberOfPages = sections + + var resetPageViewController = false + + if let viewController = pageViewController.viewControllers?.first as? GamesCollectionViewController, let gameCollection = viewController.gameCollection + { + if let index = self.fetchedResultsController.fetchedObjects?.indexOf({ $0 as! GameCollection == gameCollection }) + { + self.pageControl.currentPage = index + } + else + { + resetPageViewController = true + + self.pageControl.currentPage = 0 + } + + } + + self.navigationController?.setToolbarHidden(sections < 2, animated: self.view.window != nil) + + if sections > 0 + { + // Reset page view controller if currently hidden or current child should view controller no longer exists + if self.pageViewController.view.hidden || resetPageViewController + { + if let viewController = self.viewControllerForIndex(0) + { + self.pageViewController.view.hidden = false + self.backgroundView.hidden = true + + self.pageViewController.setViewControllers([viewController], direction: .Forward, animated: false, completion: nil) + + self.title = viewController.title + } + } + else + { + self.pageViewController.setViewControllers(self.pageViewController.viewControllers, direction: .Forward, animated: false, completion: nil) + } + } + else + { + self.title = NSLocalizedString("Games", comment: "") + + if !self.pageViewController.view.hidden + { + self.pageViewController.view.hidden = true + self.backgroundView.hidden = false + } + } + } } extension GamesViewController: GamePickerControllerDelegate @@ -85,15 +206,32 @@ extension GamesViewController: UIPageViewControllerDelegate, UIPageViewControlle { func pageViewController(pageViewController: UIPageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? { - let index = self.supportedGameTypeIdentifiers.indexOf((viewController as! GamesCollectionViewController).gameTypeIdentifier) - let viewController = self.viewControllerForIndex(index! - 1) + let viewController = self.viewControllerForIndex(self.pageControl.currentPage - 1) return viewController } func pageViewController(pageViewController: UIPageViewController, viewControllerAfterViewController viewController: UIViewController) -> UIViewController? { - let index = self.supportedGameTypeIdentifiers.indexOf((viewController as! GamesCollectionViewController).gameTypeIdentifier) - let viewController = self.viewControllerForIndex(index! + 1) + let viewController = self.viewControllerForIndex(self.pageControl.currentPage + 1) return viewController } + + func pageViewController(pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) + { + if let viewController = pageViewController.viewControllers?.first as? GamesCollectionViewController, let gameCollection = viewController.gameCollection + { + let index = self.fetchedResultsController.fetchedObjects?.indexOf({ $0 as! GameCollection == gameCollection }) ?? 0 + self.pageControl.currentPage = index + } + + self.title = pageViewController.viewControllers?.first?.title + } +} + +extension GamesViewController: NSFetchedResultsControllerDelegate +{ + func controllerDidChangeContent(controller: NSFetchedResultsController) + { + self.updateSections() + } } diff --git a/DeltaTV/GameSelectionViewController.swift b/DeltaTV/GameSelectionViewController.swift index 1628b3f..f75b71f 100644 --- a/DeltaTV/GameSelectionViewController.swift +++ b/DeltaTV/GameSelectionViewController.swift @@ -11,9 +11,12 @@ import CoreData import SNESDeltaCore +import Roxas + class GameSelectionViewController: UICollectionViewController { private let dataSource = GameCollectionViewDataSource() + private var backgroundView: RSTBackgroundView! = nil required init?(coder aDecoder: NSCoder) { @@ -21,7 +24,7 @@ class GameSelectionViewController: UICollectionViewController self.title = NSLocalizedString("Games", comment: "") - self.dataSource.gameTypeIdentifiers = [kUTTypeSNESGame as String] + self.dataSource.supportedGameCollectionIdentifiers = [kUTTypeSNESGame as String, kUTTypeGBAGame as String] self.dataSource.fetchedResultsController.delegate = self self.dataSource.cellConfigurationHandler = self.configureCell } @@ -37,11 +40,18 @@ class GameSelectionViewController: UICollectionViewController { layout.maximumBoxArtSize = CGSize(width: 200, height: 200) } + + self.backgroundView = RSTBackgroundView(frame: self.view.bounds) + self.backgroundView.autoresizingMask = [.FlexibleWidth, .FlexibleHeight] + self.backgroundView.textLabel.text = NSLocalizedString("No Games", comment: "") + self.backgroundView.detailTextLabel.text = NSLocalizedString("You can import games by pressing the + button in the top right.", comment: "") + self.view.addSubview(self.backgroundView) } override func viewWillAppear(animated: Bool) { self.dataSource.update() + self.updateCollections() super.viewWillAppear(animated) } @@ -83,6 +93,23 @@ class GameSelectionViewController: UICollectionViewController } } +private extension GameSelectionViewController +{ + func updateCollections() + { + if self.dataSource.fetchedResultsController.sections?.count ?? 0 == 0 + { + self.backgroundView.hidden = false + self.collectionView?.hidden = true + } + else + { + self.backgroundView.hidden = true + self.collectionView?.hidden = false + } + } +} + // MARK: - - extension GameSelectionViewController: GamePickerControllerDelegate { @@ -97,6 +124,8 @@ extension GameSelectionViewController: NSFetchedResultsControllerDelegate func controllerDidChangeContent(controller: NSFetchedResultsController) { self.collectionView?.reloadData() + + self.updateCollections() } } diff --git a/External/Roxas b/External/Roxas index 9789612..12973b6 160000 --- a/External/Roxas +++ b/External/Roxas @@ -1 +1 @@ -Subproject commit 9789612ecbdfd17a8c94b52f4872ec70d44cc4aa +Subproject commit 12973b60eeddd6e5b3b398d5ec5b2dadd461810d