GBA002/Delta/Game Selection/GamesViewController.swift

482 lines
18 KiB
Swift

//
// GamesViewController.swift
// Delta
//
// Created by Riley Testut on 10/12/15.
// Copyright © 2015 Riley Testut. All rights reserved.
//
import UIKit
import CoreData
import MobileCoreServices
import DeltaCore
import Roxas
class GamesViewController: UIViewController
{
var theme: Theme = .opaque {
didSet {
self.updateTheme()
}
}
weak var activeEmulatorCore: EmulatorCore? {
didSet
{
let game = oldValue?.game as? Game
NotificationCenter.default.removeObserver(self, name: .NSManagedObjectContextObjectsDidChange, object: game?.managedObjectContext)
if let game = self.activeEmulatorCore?.game as? Game
{
NotificationCenter.default.addObserver(self, selector: #selector(GamesViewController.managedObjectContextDidChange(with:)), name: .NSManagedObjectContextObjectsDidChange, object: game.managedObjectContext)
}
}
}
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
private var pageViewController: UIPageViewController!
private var placeholderView: RSTPlaceholderView!
private var pageControl: UIPageControl!
private let fetchedResultsController: NSFetchedResultsController<NSFetchRequestResult>
private var searchController: RSTSearchController?
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
fatalError("initWithNibName: not implemented")
}
required init?(coder aDecoder: NSCoder)
{
let fetchRequest = GameCollection.rst_fetchRequest()
fetchRequest.sortDescriptors = [NSSortDescriptor(key: #keyPath(GameCollection.index), ascending: true)]
self.fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext, sectionNameKeyPath: nil, cacheName: nil)
super.init(coder: aDecoder)
self.fetchedResultsController.delegate = self
self.automaticallyAdjustsScrollViewInsets = false
}
}
//MARK: - UIViewController -
/// UIViewController
extension GamesViewController
{
override func viewDidLoad()
{
super.viewDidLoad()
self.placeholderView = RSTPlaceholderView(frame: self.view.bounds)
self.placeholderView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.placeholderView.textLabel.text = NSLocalizedString("No Games", comment: "")
self.placeholderView.detailTextLabel.text = NSLocalizedString("You can import games by pressing the + button in the top right.", comment: "")
self.view.insertSubview(self.placeholderView, at: 0)
self.pageControl = UIPageControl()
self.pageControl.translatesAutoresizingMaskIntoConstraints = false
self.pageControl.hidesForSinglePage = false
self.pageControl.numberOfPages = 3
self.pageControl.currentPageIndicatorTintColor = UIColor.deltaPurple
self.pageControl.pageIndicatorTintColor = UIColor.lightGray
self.navigationController?.toolbar.addSubview(self.pageControl)
self.pageControl.centerXAnchor.constraint(equalTo: (self.navigationController?.toolbar.centerXAnchor)!, constant: 0).isActive = true
self.pageControl.centerYAnchor.constraint(equalTo: (self.navigationController?.toolbar.centerYAnchor)!, constant: 0).isActive = true
self.navigationController?.navigationBar.barStyle = .blackTranslucent
self.navigationController?.toolbar.barStyle = .blackTranslucent
if #available(iOS 11.0, *)
{
self.prepareSearchController()
}
self.updateTheme()
}
override func viewWillAppear(_ animated: Bool)
{
super.viewWillAppear(animated)
if self.fetchedResultsController.performFetchIfNeeded()
{
self.updateSections(animated: false)
}
DispatchQueue.global().async {
self.activeEmulatorCore?.stop()
}
}
override func viewDidLayoutSubviews()
{
super.viewDidLayoutSubviews()
if #available(iOS 11.0, *) {}
else
{
if let viewControllers = self.pageViewController.viewControllers as? [GameCollectionViewController]
{
for viewController in viewControllers
{
viewController.collectionView?.contentInset.top = self.topLayoutGuide.length
}
}
}
}
override func didReceiveMemoryWarning()
{
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
// MARK: - Segues -
/// Segues
extension GamesViewController
{
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?)
{
guard let identifier = segue.identifier, identifier == "embedPageViewController" else { return }
self.pageViewController = segue.destination as! UIPageViewController
self.pageViewController.dataSource = self
self.pageViewController.delegate = self
self.pageViewController.view.isHidden = true
}
@IBAction private func unwindFromSettingsViewController(_ segue: UIStoryboardSegue)
{
}
}
// MARK: - UI -
/// UI
private extension GamesViewController
{
@available(iOS 11.0, *)
func prepareSearchController()
{
let searchResultsController = self.storyboard?.instantiateViewController(withIdentifier: "gameCollectionViewController") as! GameCollectionViewController
searchResultsController.gameCollection = nil
searchResultsController.theme = self.theme
searchResultsController.activeEmulatorCore = self.activeEmulatorCore
let placeholderView = RSTPlaceholderView()
placeholderView.textLabel.text = NSLocalizedString("No Games Found", comment: "")
placeholderView.detailTextLabel.text = NSLocalizedString("Please make sure the name is correct, or try searching for another game.", comment: "")
switch self.theme
{
case .opaque: searchResultsController.dataSource.placeholderView = placeholderView
case .translucent:
let vibrancyView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: UIBlurEffect(style: .dark)))
vibrancyView.contentView.addSubview(placeholderView, pinningEdgesWith: .zero)
searchResultsController.dataSource.placeholderView = vibrancyView
}
self.searchController = RSTSearchController(searchResultsController: searchResultsController)
self.searchController?.searchableKeyPaths = [#keyPath(Game.name)]
self.searchController?.searchHandler = { [weak searchController, weak searchResultsController] (searchValue, _) in
if searchController?.searchBar.text?.isEmpty == false
{
self.pageViewController.view.isHidden = true
}
else
{
self.pageViewController.view.isHidden = false
}
searchResultsController?.dataSource.predicate = searchValue.predicate
return nil
}
self.navigationItem.searchController = self.searchController
self.navigationItem.hidesSearchBarWhenScrolling = false
self.definesPresentationContext = true
}
func updateTheme()
{
switch self.theme
{
case .opaque: self.view.backgroundColor = UIColor.deltaDarkGray
case .translucent: self.view.backgroundColor = nil
}
if let viewControllers = self.pageViewController.viewControllers as? [GameCollectionViewController]
{
for collectionViewController in viewControllers
{
collectionViewController.theme = self.theme
}
}
}
}
// MARK: - Helper Methods -
private extension GamesViewController
{
func viewControllerForIndex(_ index: Int) -> GameCollectionViewController?
{
guard let pages = self.fetchedResultsController.sections?.first?.numberOfObjects, 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 = pages + safeIndex
}
let indexPath = IndexPath(row: safeIndex, section: 0)
let viewController = self.storyboard?.instantiateViewController(withIdentifier: "gameCollectionViewController") as! GameCollectionViewController
viewController.gameCollection = self.fetchedResultsController.object(at: indexPath) as? GameCollection
viewController.theme = self.theme
viewController.activeEmulatorCore = self.activeEmulatorCore
if #available(iOS 11.0, *) {}
else
{
// 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(animated: Bool)
{
let sections = self.fetchedResultsController.sections?.first?.numberOfObjects ?? 0
self.pageControl.numberOfPages = sections
var resetPageViewController = false
if let viewController = self.pageViewController.viewControllers?.first as? GameCollectionViewController, let gameCollection = viewController.gameCollection
{
if let index = self.fetchedResultsController.fetchedObjects?.index(where: { $0 as! GameCollection == gameCollection })
{
self.pageControl.currentPage = index
}
else
{
resetPageViewController = true
self.pageControl.currentPage = 0
}
}
self.navigationController?.setToolbarHidden(sections < 2, animated: animated)
if sections > 0
{
// Reset page view controller if currently hidden or current child should view controller no longer exists
if self.pageViewController.view.isHidden || resetPageViewController
{
var index = 0
if let gameCollection = Settings.previousGameCollection
{
if let gameCollectionIndex = self.fetchedResultsController.fetchedObjects?.index(where: { $0 as! GameCollection == gameCollection })
{
index = gameCollectionIndex
}
}
if let viewController = self.viewControllerForIndex(index)
{
self.pageViewController.view.setHidden(false, animated: animated)
self.placeholderView.setHidden(true, animated: animated)
self.pageViewController.setViewControllers([viewController], direction: .forward, animated: false, completion: nil)
self.title = viewController.title
self.pageControl.currentPage = index
}
}
else
{
self.pageViewController.setViewControllers(self.pageViewController.viewControllers, direction: .forward, animated: false, completion: nil)
}
}
else
{
self.title = NSLocalizedString("Games", comment: "")
self.pageViewController.view.setHidden(true, animated: animated)
self.placeholderView.setHidden(false, animated: animated)
}
}
}
//MARK: - Importing -
/// Importing
extension GamesViewController: ImportControllerDelegate
{
@IBAction private func importFiles()
{
var documentTypes = Set(System.supportedSystems.map { $0.gameType.rawValue })
documentTypes.insert(kUTTypeZipArchive as String)
// Add GBA4iOS's exported UTIs in case user has GBA4iOS installed (which may override Delta's UTI declarations)
documentTypes.insert("com.rileytestut.gba")
documentTypes.insert("com.rileytestut.gbc")
documentTypes.insert("com.rileytestut.gb")
let itunesImportOption = iTunesImportOption(presentingViewController: self)
let importController = ImportController(documentTypes: documentTypes)
importController.delegate = self
importController.importOptions = [itunesImportOption]
self.present(importController, animated: true, completion: nil)
}
func importController(_ importController: ImportController, didImportItemsAt urls: Set<URL>, errors: [Error])
{
for error in errors
{
print(error)
}
let gameURLs = urls.filter { $0.pathExtension.lowercased() != "deltaskin" }
DatabaseManager.shared.importGames(at: Set(gameURLs)) { (games, errors) in
if errors.count > 0
{
let alertController = UIAlertController.alertController(for: .games, with: errors)
self.present(alertController, animated: true, completion: nil)
}
if games.count > 0
{
print("Imported Games:", games.map { $0.name })
}
}
let controllerSkinURLs = urls.filter { $0.pathExtension.lowercased() == "deltaskin" }
DatabaseManager.shared.importControllerSkins(at: Set(controllerSkinURLs)) { (controllerSkins, errors) in
if errors.count > 0
{
let alertController = UIAlertController.alertController(for: .controllerSkins, with: errors)
self.present(alertController, animated: true, completion: nil)
}
if controllerSkins.count > 0
{
print("Imported Controller Skins:", controllerSkins.map { $0.name })
}
}
}
}
private extension GamesViewController
{
@objc func managedObjectContextDidChange(with notification: Notification)
{
guard let deletedObjects = notification.userInfo?[NSDeletedObjectsKey] as? Set<NSManagedObject> else { return }
if let game = self.activeEmulatorCore?.game as? Game
{
if deletedObjects.contains(game)
{
DispatchQueue.main.async {
self.theme = .opaque
}
}
}
else
{
DispatchQueue.main.async {
self.theme = .opaque
}
}
}
}
//MARK: - UIPageViewController -
/// UIPageViewController
extension GamesViewController: UIPageViewControllerDataSource, UIPageViewControllerDelegate
{
//MARK: - UIPageViewControllerDataSource
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController?
{
let viewController = self.viewControllerForIndex(self.pageControl.currentPage - 1)
return viewController
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController?
{
let viewController = self.viewControllerForIndex(self.pageControl.currentPage + 1)
return viewController
}
//MARK: - UIPageViewControllerDelegate
func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController])
{
guard let viewControllers = pendingViewControllers as? [GameCollectionViewController] else { return }
if #available(iOS 11.0, *) {}
else
{
for viewController in viewControllers
{
viewController.collectionView?.contentInset.top = self.topLayoutGuide.length
}
}
}
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool)
{
if let viewController = pageViewController.viewControllers?.first as? GameCollectionViewController, let gameCollection = viewController.gameCollection
{
let index = self.fetchedResultsController.fetchedObjects?.index(where: { $0 as! GameCollection == gameCollection }) ?? 0
self.pageControl.currentPage = index
Settings.previousGameCollection = gameCollection
}
else
{
Settings.previousGameCollection = nil
}
self.title = pageViewController.viewControllers?.first?.title
}
}
extension GamesViewController: UISearchResultsUpdating
{
func updateSearchResults(for searchController: UISearchController)
{
if searchController.searchBar.text?.isEmpty == false
{
self.pageViewController.view.isHidden = true
}
else
{
self.pageViewController.view.isHidden = false
}
}
}
//MARK: - NSFetchedResultsControllerDelegate -
/// NSFetchedResultsControllerDelegate
extension GamesViewController: NSFetchedResultsControllerDelegate
{
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>)
{
self.updateSections(animated: true)
}
}