// // AppIconShortcutsViewController.swift // Delta // // Created by Riley Testut on 12/19/17. // Copyright © 2017 Riley Testut. All rights reserved. // import UIKit import DeltaCore import Roxas @objc(SwitchTableViewCell) private class SwitchTableViewCell: UITableViewCell { @IBOutlet var switchView: UISwitch! } class AppIconShortcutsViewController: UITableViewController { private lazy var dataSource = RSTCompositeTableViewPrefetchingDataSource(dataSources: [self.modeDataSource, self.shortcutsDataSource, self.gamesDataSource]) private let modeDataSource = RSTDynamicTableViewDataSource() private let shortcutsDataSource = RSTArrayTableViewPrefetchingDataSource(items: []) private let gamesDataSource = RSTFetchedResultsTableViewPrefetchingDataSource(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.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) { cell.nameLabel.textColor = .darkText cell.backgroundColor = .white 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() } }