As of iOS 13.3.1, apps installed with free developer accounts that contain embedded frameworks fail to launch. To work around this, we now link all dependencies via Cocoapods as static libraries.
363 lines
13 KiB
Swift
363 lines
13 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)
|
|
{
|
|
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()
|
|
}
|
|
}
|