diff --git a/Common/Components/Action.swift b/Common/Components/Action.swift index d6b9068..34cf937 100644 --- a/Common/Components/Action.swift +++ b/Common/Components/Action.swift @@ -16,28 +16,26 @@ extension Action case cancel case destructive case selected - } -} - -extension Action.Style -{ - var alertActionStyle: UIAlertActionStyle - { - switch self + + var alertActionStyle: UIAlertActionStyle { - case .default, .selected: return .default - case .cancel: return .cancel - case .destructive: return .destructive + switch self + { + case .default, .selected: return .default + case .cancel: return .cancel + case .destructive: return .destructive + } } - } - - var previewActionStyle: UIPreviewActionStyle - { - switch self + + var previewActionStyle: UIPreviewActionStyle? { - case .default, .cancel: return .default - case .destructive: return .destructive - case .selected: return .selected + switch self + { + case .default: return .default + case .destructive: return .destructive + case .selected: return .selected + case .cancel: return nil + } } } } @@ -47,38 +45,52 @@ struct Action let title: String let style: Style let action: ((Action) -> Void)? - - var alertAction: UIAlertAction - { - let alertAction = UIAlertAction(title: self.title, style: self.style.alertActionStyle) { (action) in - self.action?(self) - } - return alertAction - } - - var previewAction: UIPreviewAction - { - let previewAction = UIPreviewAction(title: self.title, style: self.style.previewActionStyle) { (action, viewController) in - self.action?(self) - } - return previewAction - } } -// There is no public designated initializer for UIAlertAction or UIPreviewAction, so we cannot add our own convenience init -// If only there were factory initializers... https://github.com/apple/swift-evolution/pull/247 -/* extension UIAlertAction { - convenience init(action: Action) + convenience init(_ action: Action) { + self.init(title: action.title, style: action.style.alertActionStyle) { (alertAction) in + action.action?(action) + } } } extension UIPreviewAction { - convenience init(action: Action) + convenience init?(_ action: Action) { + guard let previewActionStyle = action.style.previewActionStyle else { return nil } + + self.init(title: action.title, style: previewActionStyle) { (previewAction, viewController) in + action.action?(action) + } + } +} + +extension UIAlertController +{ + convenience init(actions: [Action]) + { + self.init(title: nil, message: nil, preferredStyle: .actionSheet) + + for action in actions.alertActions + { + self.addAction(action) + } + } +} + +extension RangeReplaceableCollection where Iterator.Element == Action +{ + var alertActions: [UIAlertAction] { + let actions = self.map { UIAlertAction($0) } + return actions + } + + var previewActions: [UIPreviewAction] { + let actions = self.flatMap { UIPreviewAction($0) } + return actions } } -*/ diff --git a/Common/Database/Model/Game.swift b/Common/Database/Model/Game.swift index 77a52e7..675f2aa 100644 --- a/Common/Database/Model/Game.swift +++ b/Common/Database/Model/Game.swift @@ -41,6 +41,7 @@ class Game: NSManagedObject, GameProtocol @NSManaged var gameCollections: Set @NSManaged var previewSaveState: SaveState? + @NSManaged var saveStates: Set var fileURL: URL { var fileURL: URL! @@ -87,6 +88,13 @@ extension Game managedObjectContext.delete(collection) } + // Manually cascade deletion since SaveState.fileURL references Game, and so we need to ensure we delete SaveState's before Game + // Otherwise, we crash when accessing SaveState.game since it is nil + for saveState in self.saveStates + { + managedObjectContext.delete(saveState) + } + if managedObjectContext.hasChanges { managedObjectContext.saveWithErrorLogging() diff --git a/Delta/Emulation/GameViewController.swift b/Delta/Emulation/GameViewController.swift index dd84b43..b16698a 100644 --- a/Delta/Emulation/GameViewController.swift +++ b/Delta/Emulation/GameViewController.swift @@ -37,9 +37,15 @@ class GameViewController: DeltaCore.GameViewController override var game: GameProtocol? { willSet { self.emulatorCore?.removeObserver(self, forKeyPath: #keyPath(EmulatorCore.state), context: &kvoContext) + + let game = self.game as? Game + NotificationCenter.default.removeObserver(self, name: .NSManagedObjectContextDidSave, object: game?.managedObjectContext) } didSet { self.emulatorCore?.addObserver(self, forKeyPath: #keyPath(EmulatorCore.state), options: [.old], context: &kvoContext) + + let game = self.game as? Game + NotificationCenter.default.addObserver(self, selector: #selector(GameViewController.managedObjectContextDidChange(with:)), name: .NSManagedObjectContextObjectsDidChange, object: game?.managedObjectContext) } } @@ -713,4 +719,16 @@ private extension GameViewController { self.updateAutoSaveState() } + + @objc func managedObjectContextDidChange(with notification: Notification) + { + guard let deletedObjects = notification.userInfo?[NSDeletedObjectsKey] as? Set else { return } + guard let game = self.game as? Game else { return } + + if deletedObjects.contains(game) + { + self.emulatorCore?.gameViews.forEach { $0.inputImage = nil } + self.game = nil + } + } } diff --git a/Delta/Game Selection/GameCollectionViewController.swift b/Delta/Game Selection/GameCollectionViewController.swift index 96aaa60..401df20 100644 --- a/Delta/Game Selection/GameCollectionViewController.swift +++ b/Delta/Game Selection/GameCollectionViewController.swift @@ -53,6 +53,9 @@ extension GameCollectionViewController layout.itemWidth = 90 self.registerForPreviewing(with: self, sourceView: self.collectionView!) + + let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(GameCollectionViewController.handleLongPressGesture(_:))) + self.collectionView?.addGestureRecognizer(longPressGestureRecognizer) } override func viewWillAppear(_ animated: Bool) @@ -187,6 +190,52 @@ private extension GameCollectionViewController } } +//MARK: - Game Actions - +private extension GameCollectionViewController +{ + func actions(for game: Game) -> [Action] + { + let cancelAction = Action(title: NSLocalizedString("Cancel", comment: ""), style: .cancel, action: nil) + + let deleteAction = Action(title: NSLocalizedString("Delete", comment: ""), style: .destructive, action: { [unowned self] action in + self.delete(game) + }) + + return [cancelAction, deleteAction] + } + + func delete(_ game: Game) + { + let confirmationAlertController = UIAlertController(title: NSLocalizedString("Are you sure you want to delete this game? All associated data, such as saves, save states, and cheat codes, will also be deleted.", comment: ""), message: nil, preferredStyle: .actionSheet) + confirmationAlertController.addAction(UIAlertAction(title: NSLocalizedString("Delete Game", comment: ""), style: .destructive, handler: { action in + + DatabaseManager.shared.performBackgroundTask { (context) in + let temporaryGame = context.object(with: game.objectID) as! Game + context.delete(temporaryGame) + + context.saveWithErrorLogging() + } + + })) + confirmationAlertController.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel, handler: nil)) + + self.present(confirmationAlertController, animated: true, completion: nil) + } + + @objc func handleLongPressGesture(_ gestureRecognizer: UILongPressGestureRecognizer) + { + guard gestureRecognizer.state == .began else { return } + + guard let indexPath = self.collectionView?.indexPathForItem(at: gestureRecognizer.location(in: self.collectionView)) else { return } + + let game = self.dataSource.fetchedResultsController.object(at: indexPath) + let actions = self.actions(for: game) + + let alertController = UIAlertController(actions: actions) + self.present(alertController, animated: true, completion: nil) + } +} + //MARK: - UIViewControllerPreviewingDelegate - /// UIViewControllerPreviewingDelegate extension GameCollectionViewController: UIViewControllerPreviewingDelegate @@ -212,6 +261,9 @@ extension GameCollectionViewController: UIViewControllerPreviewingDelegate gameViewController.previewImage = UIImage(contentsOfFile: previewSaveState.imageFileURL.path) } + let actions = self.actions(for: game).previewActions + gameViewController.overridePreviewActionItems = actions + return gameViewController } diff --git a/Delta/Game Selection/GamesViewController.swift b/Delta/Game Selection/GamesViewController.swift index 56b9dff..c5ebb2d 100644 --- a/Delta/Game Selection/GamesViewController.swift +++ b/Delta/Game Selection/GamesViewController.swift @@ -89,7 +89,7 @@ extension GamesViewController if self.fetchedResultsController.performFetchIfNeeded() { - self.updateSections() + self.updateSections(animated: false) } DispatchQueue.global().async { @@ -189,10 +189,13 @@ private extension GamesViewController viewController.theme = self.theme viewController.activeEmulatorCore = self.activeEmulatorCore + // Need to set content inset here AND willTransitionTo callback to ensure its correct for all edge cases + viewController.collectionView?.contentInset.top = self.topLayoutGuide.length + return viewController } - func updateSections() + func updateSections(animated: Bool) { let sections = self.fetchedResultsController.sections?.first?.numberOfObjects ?? 0 self.pageControl.numberOfPages = sections @@ -214,7 +217,7 @@ private extension GamesViewController } - self.navigationController?.setToolbarHidden(sections < 2, animated: self.view.window != nil) + self.navigationController?.setToolbarHidden(sections < 2, animated: animated) if sections > 0 { @@ -223,8 +226,8 @@ private extension GamesViewController { if let viewController = self.viewControllerForIndex(0) { - self.pageViewController.view.isHidden = false - self.backgroundView.isHidden = true + self.pageViewController.view.setHidden(false, animated: animated) + self.backgroundView.setHidden(true, animated: animated) self.pageViewController.setViewControllers([viewController], direction: .forward, animated: false, completion: nil) @@ -240,11 +243,8 @@ private extension GamesViewController { self.title = NSLocalizedString("Games", comment: "") - if !self.pageViewController.view.isHidden - { - self.pageViewController.view.isHidden = true - self.backgroundView.isHidden = false - } + self.pageViewController.view.setHidden(true, animated: animated) + self.backgroundView.setHidden(false, animated: animated) } } } @@ -313,6 +313,6 @@ extension GamesViewController: NSFetchedResultsControllerDelegate { func controllerDidChangeContent(_ controller: NSFetchedResultsController) { - self.updateSections() + self.updateSections(animated: true) } } diff --git a/Delta/Pause Menu/Save States/SaveStatesViewController.swift b/Delta/Pause Menu/Save States/SaveStatesViewController.swift index 9323753..ecb42a7 100644 --- a/Delta/Pause Menu/Save States/SaveStatesViewController.swift +++ b/Delta/Pause Menu/Save States/SaveStatesViewController.swift @@ -251,15 +251,9 @@ private extension SaveStatesViewController let saveState = self.fetchedResultsController.object(at: indexPath) as! SaveState - guard let actions = self.actionsForSaveState(saveState)?.map({ $0.alertAction }) else { return } - - let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) - - for action in actions - { - alertController.addAction(action) - } + guard let actions = self.actionsForSaveState(saveState) else { return } + let alertController = UIAlertController(actions: actions) self.present(alertController, animated: true, completion: nil) } @@ -559,10 +553,11 @@ extension SaveStatesViewController: UIViewControllerPreviewingDelegate self.emulatorCoreSaveState != nil else { return nil } + previewingContext.sourceRect = layoutAttributes.frame let saveState = self.fetchedResultsController.object(at: indexPath) as! SaveState - let actions = self.actionsForSaveState(saveState)?.lazy.filter{ $0.style != .cancel }.map{ $0.previewAction } ?? [] + let actions = self.actionsForSaveState(saveState)?.previewActions ?? [] let previewImage = self.imageCache.object(forKey: saveState.imageFileURL) ?? UIImage(contentsOfFile: saveState.imageFileURL.path) let previewGameViewController = PreviewGameViewController() diff --git a/External/Roxas b/External/Roxas index f0661f7..e334730 160000 --- a/External/Roxas +++ b/External/Roxas @@ -1 +1 @@ -Subproject commit f0661f78b095212ed2f564a4bbf1c1f6c9c50384 +Subproject commit e334730200712202feb470f1525c7eea30542146