GBA002/Delta/Settings/App Icon Shortcuts/AppIconShortcutsViewController.swift
2020-11-06 11:45:25 -05:00

366 lines
14 KiB
Swift

//
// AppIconShortcutsViewController.swift
// Delta
//
// Created by Riley Testut on 12/19/17.
// Copyright © 2017 Riley Testut. All rights reserved.
//
import UIKit
import AVFoundation
import DeltaCore
import Roxas
@objc(SwitchTableViewCell)
private class SwitchTableViewCell: UITableViewCell
{
@IBOutlet var switchView: UISwitch!
}
class AppIconShortcutsViewController: UITableViewController
{
private lazy var dataSource = RSTCompositeTableViewPrefetchingDataSource<Game, UIImage>(dataSources: [self.modeDataSource, self.shortcutsDataSource, self.gamesDataSource])
private let modeDataSource = RSTDynamicTableViewDataSource<Game>()
private let shortcutsDataSource = RSTArrayTableViewPrefetchingDataSource<Game, UIImage>(items: [])
private let gamesDataSource = RSTFetchedResultsTableViewPrefetchingDataSource<Game, UIImage>(fetchedResultsController: NSFetchedResultsController())
required init?(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder)
self.prepareDataSource()
}
override func viewDidLoad()
{
super.viewDidLoad()
self.tableView.register(GameTableViewCell.nib!, forCellReuseIdentifier: RSTCellContentGenericCellIdentifier)
self.navigationItem.searchController = self.gamesDataSource.searchController
self.navigationItem.hidesSearchBarWhenScrolling = false
self.tableView.dataSource = self.dataSource
self.tableView.prefetchDataSource = self.dataSource
self.tableView.allowsSelectionDuringEditing = true
self.updateShortcuts()
}
}
private extension AppIconShortcutsViewController
{
func prepareDataSource()
{
// Mode
self.modeDataSource.numberOfSectionsHandler = { 1 }
self.modeDataSource.numberOfItemsHandler = { [weak self] _ in (self?.gamesDataSource.itemCount ?? 0) > 0 ? 1 : 0 }
self.modeDataSource.cellIdentifierHandler = { _ in "SwitchCell" }
// Shortcuts
self.shortcutsDataSource.items = Settings.gameShortcuts
// Games
let gamesFetchRequest: NSFetchRequest<Game> = Game.fetchRequest()
gamesFetchRequest.returnsObjectsAsFaults = false
gamesFetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \Game.type, ascending: true), NSSortDescriptor(key: #keyPath(Game.name), ascending: true)]
let gamesFetchedResultsController = NSFetchedResultsController(fetchRequest: gamesFetchRequest, managedObjectContext: DatabaseManager.shared.viewContext, sectionNameKeyPath: #keyPath(Game.type), cacheName: nil)
self.gamesDataSource.fetchedResultsController = gamesFetchedResultsController
self.gamesDataSource.searchController.searchableKeyPaths = [#keyPath(Game.name)]
// Data Source
self.dataSource.proxy = self
self.dataSource.cellConfigurationHandler = { [unowned self] (cell, game, indexPath) in
if indexPath.section == 0
{
self.configureModeCell(cell as! SwitchTableViewCell, for: indexPath)
}
else
{
self.configureGameCell(cell as! GameTableViewCell, with: game, for: indexPath)
}
}
self.dataSource.prefetchHandler = { (game, indexPath, completionHandler) in
guard indexPath.section > 0 else { return nil }
guard let artworkURL = game.artworkURL else { return nil }
let imageOperation = LoadImageURLOperation(url: artworkURL)
imageOperation.resultHandler = { (image, error) in
completionHandler(image, error)
}
return imageOperation
}
self.dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
guard indexPath.section > 0 else { return }
guard let image = image else { return }
let cell = cell as! GameTableViewCell
let artworkDisplaySize = AVMakeRect(aspectRatio: image.size, insideRect: cell.artworkImageView.bounds)
let offset = (cell.artworkImageView.bounds.width - artworkDisplaySize.width) / 2
// Offset artworkImageViewLeadingConstraint and artworkImageViewTrailingConstraint to right-align artworkImageView
cell.artworkImageViewLeadingConstraint.constant += offset
cell.artworkImageViewTrailingConstraint.constant -= offset
cell.artworkImageView.image = image
cell.artworkImageView.superview?.layoutIfNeeded()
}
self.dataSource.rowAnimation = .fade
let placeholderView = RSTPlaceholderView()
placeholderView.textLabel.text = NSLocalizedString("No App Icon Shortcuts", comment: "")
placeholderView.detailTextLabel.text = NSLocalizedString("You can customize the shortcuts that appear when 3D Touching the app icon once you've added some games.", comment: "")
self.dataSource.placeholderView = placeholderView
}
func configureModeCell(_ cell: SwitchTableViewCell, for indexPath: IndexPath)
{
cell.textLabel?.text = NSLocalizedString("Recently Played Games", comment: "")
cell.textLabel?.backgroundColor = .clear
cell.switchView.isOn = (Settings.gameShortcutsMode == .recent)
cell.switchView.onTintColor = self.view.tintColor
}
func configureGameCell(_ cell: GameTableViewCell, with game: Game, for indexPath: IndexPath)
{
if #available(iOS 13.0, *) {
cell.nameLabel?.textColor = .label
} else {
cell.nameLabel?.textColor = .darkText
}
cell.nameLabel.text = game.name
cell.artworkImageView.image = #imageLiteral(resourceName: "BoxArt")
cell.artworkImageViewLeadingConstraint.constant = 15
cell.artworkImageViewTrailingConstraint.constant = 15
cell.separatorInset.left = cell.nameLabel.frame.minX
cell.selectedBackgroundView = nil
switch (indexPath.section, Settings.gameShortcutsMode)
{
case (1, _):
cell.selectionStyle = .none
cell.contentView.alpha = 1.0
case (2..., .recent):
cell.selectionStyle = .none
cell.contentView.alpha = 0.3
case (2..., .manual):
cell.selectionStyle = .gray
cell.contentView.alpha = 1.0
default: break
}
}
}
private extension AppIconShortcutsViewController
{
func updateShortcuts()
{
switch Settings.gameShortcutsMode
{
case .recent:
let fetchRequest = Game.recentlyPlayedFetchRequest
fetchRequest.returnsObjectsAsFaults = false
do
{
let games = try DatabaseManager.shared.viewContext.fetch(fetchRequest)
self.shortcutsDataSource.setItems(games, with: [])
}
catch
{
print(error)
}
self.tableView.setEditing(false, animated: true)
case .manual: self.tableView.setEditing(true, animated: true)
}
Settings.gameShortcuts = self.shortcutsDataSource.items
}
func addShortcut(for game: Game)
{
guard self.shortcutsDataSource.items.count < 4 else { return }
guard !self.shortcutsDataSource.items.contains(game) else { return }
// No need to adjust destinationIndexPath, since it forwards change directly to table view.
let destinationIndexPath = IndexPath(row: self.shortcutsDataSource.items.count, section: 1)
let insertion = RSTCellContentChange(type: .insert, currentIndexPath: nil, destinationIndexPath: destinationIndexPath)
insertion.rowAnimation = .fade
var shortcuts = self.shortcutsDataSource.items
shortcuts.insert(game, at: destinationIndexPath.row)
self.shortcutsDataSource.setItems(shortcuts, with: [insertion])
self.updateShortcuts()
}
}
private extension AppIconShortcutsViewController
{
@IBAction func switchGameShortcutsMode(with sender: UISwitch)
{
if sender.isOn
{
Settings.gameShortcutsMode = .recent
}
else
{
Settings.gameShortcutsMode = .manual
}
self.tableView.beginUpdates()
self.updateShortcuts()
self.tableView.reloadSections(IndexSet(integersIn: 0 ..< self.tableView.numberOfSections), with: .fade)
self.tableView.endUpdates()
}
}
extension AppIconShortcutsViewController
{
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
{
guard indexPath.section == 0 else { return super.tableView(tableView, heightForRowAt: indexPath) }
return 44
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String?
{
guard self.dataSource.itemCount > 0 else { return nil }
switch section
{
case 0: return nil
case 1: return NSLocalizedString("Shortcuts", comment: "")
default:
let gameType = GameType(rawValue: self.gamesDataSource.fetchedResultsController.sections![section - 2].name)
let system = System(gameType: gameType)!
return system.localizedName
}
}
override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String?
{
guard self.dataSource.itemCount > 0 else { return nil }
switch (section, Settings.gameShortcutsMode)
{
case (0, .recent): return NSLocalizedString("Your most recently played games will appear as shortcuts when 3D touching the app icon.", comment: "")
case (0, .manual): return NSLocalizedString("The games you've selected below will appear as shortcuts when 3D touching the app icon.", comment: "")
case (1, .recent) where self.shortcutsDataSource.itemCount == 0: return NSLocalizedString("You have no recently played games.", comment: "")
case (1, .recent): return " " // Return non-empty string since empty string changes vertical offset of section for some reason.
case (1, .manual): return NSLocalizedString("You may have up to 4 shortcuts.", comment: "")
default: return nil
}
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
guard indexPath.section > 1 else { return }
guard Settings.gameShortcutsMode == .manual else { return }
tableView.deselectRow(at: indexPath, animated: true)
let game = self.dataSource.item(at: indexPath)
self.addShortcut(for: game)
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath)
{
switch editingStyle
{
case .none: break
case .delete:
let deletion = RSTCellContentChange(type: .delete, currentIndexPath: indexPath, destinationIndexPath: nil)
deletion.rowAnimation = .fade
var shortcuts = self.shortcutsDataSource.items
shortcuts.remove(at: indexPath.row) // No need to adjust indexPath, since it forwards change directly to table view.
self.shortcutsDataSource.setItems(shortcuts, with: [deletion])
case .insert:
let game = self.dataSource.item(at: indexPath)
self.addShortcut(for: game)
@unknown default: break
}
self.updateShortcuts()
}
override func tableView(_ tableView: UITableView, titleForDeleteConfirmationButtonForRowAt indexPath: IndexPath) -> String?
{
return NSLocalizedString("Remove", comment: "")
}
override func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle
{
switch indexPath.section
{
case 1: return .delete
case 2...: return .insert
default: return .none
}
}
override func tableView(_ tableView: UITableView, shouldIndentWhileEditingRowAt indexPath: IndexPath) -> Bool
{
return false
}
override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool
{
return (indexPath.section == 1)
}
override func tableView(_ tableView: UITableView, targetIndexPathForMoveFromRowAt sourceIndexPath: IndexPath, toProposedIndexPath proposedDestinationIndexPath: IndexPath) -> IndexPath
{
let indexPath: IndexPath
switch proposedDestinationIndexPath.section
{
case 0: indexPath = IndexPath(row: 0, section: 1)
case 1: indexPath = proposedDestinationIndexPath
default: indexPath = IndexPath(row: self.shortcutsDataSource.items.count - 1, section: 1)
}
return indexPath
}
override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath)
{
var items = self.shortcutsDataSource.items
let game = items.remove(at: sourceIndexPath.row)
items.insert(game, at: destinationIndexPath.row)
self.shortcutsDataSource.items = items
self.updateShortcuts()
}
}