Refactors UICollectionViewControllers/UITableViewControllers to use prefetching RSTCellContentDataSources
This commit is contained in:
parent
0c567de380
commit
812a773fba
@ -45,6 +45,11 @@ class LoadImageURLOperation: RSTLoadOperation<UIImage, NSURL>
|
|||||||
super.cancel()
|
super.cancel()
|
||||||
|
|
||||||
self.downloadOperation?.cancel()
|
self.downloadOperation?.cancel()
|
||||||
|
|
||||||
|
if self.isAsynchronous
|
||||||
|
{
|
||||||
|
self.finish()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func loadResult(completion: @escaping (UIImage?, Swift.Error?) -> Void)
|
override func loadResult(completion: @escaping (UIImage?, Swift.Error?) -> Void)
|
||||||
|
|||||||
@ -16,10 +16,8 @@ class GamesDatabaseBrowserViewController: UITableViewController
|
|||||||
var selectionHandler: ((GameMetadata) -> Void)?
|
var selectionHandler: ((GameMetadata) -> Void)?
|
||||||
|
|
||||||
fileprivate let database: GamesDatabase?
|
fileprivate let database: GamesDatabase?
|
||||||
fileprivate let dataSource: RSTArrayTableViewDataSource<GameMetadata>
|
|
||||||
|
|
||||||
fileprivate let operationQueue = RSTOperationQueue()
|
fileprivate let dataSource: RSTArrayTableViewPrefetchingDataSource<GameMetadata, UIImage>
|
||||||
fileprivate let imageCache = NSCache<NSURL, UIImage>()
|
|
||||||
|
|
||||||
override init(style: UITableViewStyle) {
|
override init(style: UITableViewStyle) {
|
||||||
fatalError()
|
fatalError()
|
||||||
@ -37,39 +35,13 @@ class GamesDatabaseBrowserViewController: UITableViewController
|
|||||||
print(error)
|
print(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.dataSource = RSTArrayTableViewDataSource<GameMetadata>(items: [])
|
self.dataSource = RSTArrayTableViewPrefetchingDataSource<GameMetadata, UIImage>(items: [])
|
||||||
|
|
||||||
let placeholderView = RSTPlaceholderView()
|
|
||||||
placeholderView.textLabel.textColor = UIColor.lightText
|
|
||||||
placeholderView.detailTextLabel.textColor = UIColor.lightText
|
|
||||||
|
|
||||||
self.dataSource.placeholderView = placeholderView
|
|
||||||
|
|
||||||
super.init(coder: aDecoder)
|
super.init(coder: aDecoder)
|
||||||
|
|
||||||
self.dataSource.cellConfigurationHandler = { (cell, metadata, indexPath) in
|
|
||||||
self.configure(cell: cell as! GameMetadataTableViewCell, with: metadata, for: indexPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
if let database = self.database
|
|
||||||
{
|
|
||||||
self.dataSource.searchController.searchHandler = { [unowned database, unowned dataSource] (searchValue, previousSearchValue) in
|
|
||||||
|
|
||||||
return RSTBlockOperation(executionBlock: { [unowned database, unowned dataSource] (operation) in
|
|
||||||
let results = database.metadataResults(forGameName: searchValue.text)
|
|
||||||
|
|
||||||
guard !operation.isCancelled else { return }
|
|
||||||
|
|
||||||
dataSource.items = results
|
|
||||||
|
|
||||||
rst_dispatch_sync_on_main_thread {
|
|
||||||
self.updatePlaceholderView()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.definesPresentationContext = true
|
self.definesPresentationContext = true
|
||||||
|
|
||||||
|
self.prepareDataSource()
|
||||||
}
|
}
|
||||||
|
|
||||||
override var preferredStatusBarStyle: UIStatusBarStyle {
|
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||||
@ -83,6 +55,8 @@ class GamesDatabaseBrowserViewController: UITableViewController
|
|||||||
self.view.backgroundColor = UIColor.deltaDarkGray
|
self.view.backgroundColor = UIColor.deltaDarkGray
|
||||||
|
|
||||||
self.tableView.dataSource = self.dataSource
|
self.tableView.dataSource = self.dataSource
|
||||||
|
self.tableView.prefetchDataSource = self.dataSource
|
||||||
|
|
||||||
self.tableView.indicatorStyle = .white
|
self.tableView.indicatorStyle = .white
|
||||||
self.tableView.separatorColor = UIColor.gray
|
self.tableView.separatorColor = UIColor.gray
|
||||||
|
|
||||||
@ -99,7 +73,73 @@ class GamesDatabaseBrowserViewController: UITableViewController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension GamesDatabaseBrowserViewController
|
private extension GamesDatabaseBrowserViewController
|
||||||
|
{
|
||||||
|
func prepareDataSource()
|
||||||
|
{
|
||||||
|
/* Placeholder View */
|
||||||
|
let placeholderView = RSTPlaceholderView()
|
||||||
|
placeholderView.textLabel.textColor = UIColor.lightText
|
||||||
|
placeholderView.detailTextLabel.textColor = UIColor.lightText
|
||||||
|
self.dataSource.placeholderView = placeholderView
|
||||||
|
|
||||||
|
|
||||||
|
/* Cell Configuration */
|
||||||
|
self.dataSource.cellConfigurationHandler = { [unowned self] (cell, metadata, indexPath) in
|
||||||
|
self.configure(cell: cell as! GameMetadataTableViewCell, with: metadata, for: indexPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Prefetching */
|
||||||
|
self.dataSource.prefetchHandler = { (metadata, indexPath, completionHandler) in
|
||||||
|
guard let artworkURL = metadata.artworkURL else { return nil }
|
||||||
|
|
||||||
|
let operation = LoadImageURLOperation(url: artworkURL)
|
||||||
|
operation.resultHandler = { (image, error) in
|
||||||
|
completionHandler(image, error)
|
||||||
|
}
|
||||||
|
return operation
|
||||||
|
}
|
||||||
|
|
||||||
|
self.dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
|
||||||
|
guard let image = image else { return }
|
||||||
|
|
||||||
|
let cell = cell as! GameMetadataTableViewCell
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Searching */
|
||||||
|
if let database = self.database
|
||||||
|
{
|
||||||
|
self.dataSource.searchController.searchHandler = { [unowned self, unowned database] (searchValue, previousSearchValue) in
|
||||||
|
return RSTBlockOperation() { [unowned self, unowned database] (operation) in
|
||||||
|
let results = database.metadataResults(forGameName: searchValue.text)
|
||||||
|
|
||||||
|
guard !operation.isCancelled else { return }
|
||||||
|
|
||||||
|
self.dataSource.items = results
|
||||||
|
|
||||||
|
rst_dispatch_sync_on_main_thread {
|
||||||
|
self.resetTableViewContentOffset()
|
||||||
|
self.updatePlaceholderView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension GamesDatabaseBrowserViewController
|
||||||
{
|
{
|
||||||
func configure(cell: GameMetadataTableViewCell, with metadata: GameMetadata, for indexPath: IndexPath)
|
func configure(cell: GameMetadataTableViewCell, with metadata: GameMetadata, for indexPath: IndexPath)
|
||||||
{
|
{
|
||||||
@ -112,30 +152,6 @@ extension GamesDatabaseBrowserViewController
|
|||||||
cell.artworkImageViewTrailingConstraint.constant = 15
|
cell.artworkImageViewTrailingConstraint.constant = 15
|
||||||
|
|
||||||
cell.separatorInset.left = cell.nameLabel.frame.minX
|
cell.separatorInset.left = cell.nameLabel.frame.minX
|
||||||
|
|
||||||
if let artworkURL = metadata.artworkURL
|
|
||||||
{
|
|
||||||
let operation = LoadImageURLOperation(url: artworkURL)
|
|
||||||
operation.resultsCache = self.imageCache
|
|
||||||
operation.resultHandler = { (image, error) in
|
|
||||||
if let image = image
|
|
||||||
{
|
|
||||||
let artworkDisplaySize = AVMakeRect(aspectRatio: image.size, insideRect: cell.artworkImageView.bounds)
|
|
||||||
let offset = (cell.artworkImageView.bounds.width - artworkDisplaySize.width) / 2
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
// 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.operationQueue.addOperation(operation, forKey: indexPath as NSIndexPath)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func updatePlaceholderView()
|
func updatePlaceholderView()
|
||||||
@ -153,6 +169,12 @@ extension GamesDatabaseBrowserViewController
|
|||||||
placeholderView.detailTextLabel.text = NSLocalizedString("Please make sure the name is correct, or try searching for another game.", comment: "")
|
placeholderView.detailTextLabel.text = NSLocalizedString("Please make sure the name is correct, or try searching for another game.", comment: "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resetTableViewContentOffset()
|
||||||
|
{
|
||||||
|
self.tableView.setContentOffset(CGPoint.zero, animated: false)
|
||||||
|
self.tableView.setContentOffset(CGPoint(x: 0, y: -self.topLayoutGuide.length), animated: false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension GamesDatabaseBrowserViewController
|
extension GamesDatabaseBrowserViewController
|
||||||
@ -167,12 +189,6 @@ extension GamesDatabaseBrowserViewController
|
|||||||
let metadata = self.dataSource.item(at: indexPath)
|
let metadata = self.dataSource.item(at: indexPath)
|
||||||
self.selectionHandler?(metadata)
|
self.selectionHandler?(metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath)
|
|
||||||
{
|
|
||||||
let operation = self.operationQueue[indexPath as NSIndexPath]
|
|
||||||
operation?.cancel()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension GamesDatabaseBrowserViewController: UISearchControllerDelegate
|
extension GamesDatabaseBrowserViewController: UISearchControllerDelegate
|
||||||
@ -193,7 +209,6 @@ extension GamesDatabaseBrowserViewController: UISearchControllerDelegate
|
|||||||
func didDismissSearchController(_ searchController: UISearchController)
|
func didDismissSearchController(_ searchController: UISearchController)
|
||||||
{
|
{
|
||||||
// Fix potentially incorrect offset if user dismisses searchController while scrolling
|
// Fix potentially incorrect offset if user dismisses searchController while scrolling
|
||||||
self.tableView.setContentOffset(CGPoint.zero, animated: false)
|
self.resetTableViewContentOffset()
|
||||||
self.tableView.setContentOffset(CGPoint(x: 0, y: -self.topLayoutGuide.length), animated: false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -45,17 +45,23 @@ class GameCollectionViewController: UICollectionViewController
|
|||||||
|
|
||||||
fileprivate var activeSaveState: SaveStateProtocol?
|
fileprivate var activeSaveState: SaveStateProtocol?
|
||||||
|
|
||||||
fileprivate let dataSource = RSTFetchedResultsCollectionViewDataSource<Game>(fetchedResultsController: NSFetchedResultsController())
|
fileprivate let dataSource: RSTFetchedResultsCollectionViewPrefetchingDataSource<Game, UIImage>
|
||||||
fileprivate let prototypeCell = GridCollectionViewCell()
|
fileprivate let prototypeCell = GridCollectionViewCell()
|
||||||
|
|
||||||
fileprivate let imageOperationQueue = RSTOperationQueue()
|
|
||||||
fileprivate let imageCache = NSCache<NSURL, UIImage>()
|
|
||||||
|
|
||||||
fileprivate var _performing3DTouchTransition = false
|
fileprivate var _performing3DTouchTransition = false
|
||||||
fileprivate weak var _destination3DTouchTransitionViewController: UIViewController?
|
fileprivate weak var _destination3DTouchTransitionViewController: UIViewController?
|
||||||
|
|
||||||
fileprivate var _renameAction: UIAlertAction?
|
fileprivate var _renameAction: UIAlertAction?
|
||||||
fileprivate var _changingArtworkGame: Game?
|
fileprivate var _changingArtworkGame: Game?
|
||||||
|
|
||||||
|
required init?(coder aDecoder: NSCoder)
|
||||||
|
{
|
||||||
|
self.dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<Game, UIImage>(fetchedResultsController: NSFetchedResultsController())
|
||||||
|
|
||||||
|
super.init(coder: aDecoder)
|
||||||
|
|
||||||
|
self.prepareDataSource()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//MARK: - UIViewController -
|
//MARK: - UIViewController -
|
||||||
@ -66,11 +72,8 @@ extension GameCollectionViewController
|
|||||||
{
|
{
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
self.dataSource.cellConfigurationHandler = { [unowned self] (cell, item, indexPath) in
|
|
||||||
self.configure(cell as! GridCollectionViewCell, for: indexPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
self.collectionView?.dataSource = self.dataSource
|
self.collectionView?.dataSource = self.dataSource
|
||||||
|
self.collectionView?.prefetchDataSource = self.dataSource
|
||||||
self.collectionView?.delegate = self
|
self.collectionView?.delegate = self
|
||||||
|
|
||||||
let layout = self.collectionViewLayout as! GridCollectionViewLayout
|
let layout = self.collectionViewLayout as! GridCollectionViewLayout
|
||||||
@ -182,7 +185,33 @@ extension GameCollectionViewController
|
|||||||
//MARK: - Private Methods -
|
//MARK: - Private Methods -
|
||||||
private extension GameCollectionViewController
|
private extension GameCollectionViewController
|
||||||
{
|
{
|
||||||
//MARK: - Update
|
//MARK: - Data Source
|
||||||
|
func prepareDataSource()
|
||||||
|
{
|
||||||
|
self.dataSource.cellConfigurationHandler = { [unowned self] (cell, item, indexPath) in
|
||||||
|
self.configure(cell as! GridCollectionViewCell, for: indexPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.dataSource.prefetchHandler = { (game, indexPath, completionHandler) in
|
||||||
|
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 let image = image else { return }
|
||||||
|
|
||||||
|
let cell = cell as! GridCollectionViewCell
|
||||||
|
cell.imageView.image = image
|
||||||
|
cell.isImageViewVibrancyEnabled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func updateDataSource()
|
func updateDataSource()
|
||||||
{
|
{
|
||||||
let fetchRequest: NSFetchRequest<Game> = Game.fetchRequest()
|
let fetchRequest: NSFetchRequest<Game> = Game.fetchRequest()
|
||||||
@ -194,7 +223,7 @@ private extension GameCollectionViewController
|
|||||||
}
|
}
|
||||||
|
|
||||||
//MARK: - Configure Cells
|
//MARK: - Configure Cells
|
||||||
func configure(_ cell: GridCollectionViewCell, for indexPath: IndexPath, ignoreImageOperations: Bool = false)
|
func configure(_ cell: GridCollectionViewCell, for indexPath: IndexPath)
|
||||||
{
|
{
|
||||||
let game = self.dataSource.item(at: indexPath)
|
let game = self.dataSource.item(at: indexPath)
|
||||||
|
|
||||||
@ -214,24 +243,6 @@ private extension GameCollectionViewController
|
|||||||
cell.maximumImageSize = CGSize(width: 90, height: 90)
|
cell.maximumImageSize = CGSize(width: 90, height: 90)
|
||||||
cell.textLabel.text = game.name
|
cell.textLabel.text = game.name
|
||||||
cell.textLabel.textColor = UIColor.gray
|
cell.textLabel.textColor = UIColor.gray
|
||||||
|
|
||||||
if let artworkURL = game.artworkURL, !ignoreImageOperations
|
|
||||||
{
|
|
||||||
let imageOperation = LoadImageURLOperation(url: artworkURL)
|
|
||||||
imageOperation.resultsCache = self.imageCache
|
|
||||||
imageOperation.resultHandler = { (image, error) in
|
|
||||||
|
|
||||||
if let image = image
|
|
||||||
{
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
cell.imageView.image = image
|
|
||||||
cell.isImageViewVibrancyEnabled = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.imageOperationQueue.addOperation(imageOperation, forKey: indexPath as NSCopying)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//MARK: - Emulation
|
//MARK: - Emulation
|
||||||
@ -533,11 +544,8 @@ extension GameCollectionViewController: ImportControllerDelegate
|
|||||||
|
|
||||||
if let imageURL = imageURL
|
if let imageURL = imageURL
|
||||||
{
|
{
|
||||||
if let previousArtworkURL = game.artworkURL as NSURL?
|
// Remove previous artwork from cache.
|
||||||
{
|
self.dataSource.prefetchItemCache.removeObject(forKey: game)
|
||||||
// Remove previous artwork from cache.
|
|
||||||
self.imageCache.removeObject(forKey: previousArtworkURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
DatabaseManager.shared.performBackgroundTask { (context) in
|
DatabaseManager.shared.performBackgroundTask { (context) in
|
||||||
let temporaryGame = context.object(with: game.objectID) as! Game
|
let temporaryGame = context.object(with: game.objectID) as! Game
|
||||||
@ -626,12 +634,6 @@ extension GameCollectionViewController
|
|||||||
self.launchGame(withSender: cell, clearScreen: true)
|
self.launchGame(withSender: cell, clearScreen: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath)
|
|
||||||
{
|
|
||||||
let operation = self.imageOperationQueue[indexPath as NSCopying]
|
|
||||||
operation?.cancel()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//MARK: - UICollectionViewDelegateFlowLayout -
|
//MARK: - UICollectionViewDelegateFlowLayout -
|
||||||
@ -646,7 +648,7 @@ extension GameCollectionViewController: UICollectionViewDelegateFlowLayout
|
|||||||
widthConstraint.isActive = true
|
widthConstraint.isActive = true
|
||||||
defer { widthConstraint.isActive = false }
|
defer { widthConstraint.isActive = false }
|
||||||
|
|
||||||
self.configure(self.prototypeCell, for: indexPath, ignoreImageOperations: true)
|
self.configure(self.prototypeCell, for: indexPath)
|
||||||
|
|
||||||
let size = self.prototypeCell.contentView.systemLayoutSizeFitting(UILayoutFittingCompressedSize)
|
let size = self.prototypeCell.contentView.systemLayoutSizeFitting(UILayoutFittingCompressedSize)
|
||||||
return size
|
return size
|
||||||
|
|||||||
@ -65,10 +65,7 @@ class SaveStatesViewController: UICollectionViewController
|
|||||||
fileprivate var prototypeCellWidthConstraint: NSLayoutConstraint!
|
fileprivate var prototypeCellWidthConstraint: NSLayoutConstraint!
|
||||||
fileprivate var prototypeHeader = SaveStatesCollectionHeaderView()
|
fileprivate var prototypeHeader = SaveStatesCollectionHeaderView()
|
||||||
|
|
||||||
fileprivate let dataSource = RSTFetchedResultsCollectionViewDataSource<SaveState>(fetchedResultsController: NSFetchedResultsController())
|
fileprivate let dataSource: RSTFetchedResultsCollectionViewPrefetchingDataSource<SaveState, UIImage>
|
||||||
|
|
||||||
fileprivate let imageOperationQueue = RSTOperationQueue()
|
|
||||||
fileprivate let imageCache = NSCache<NSURL, UIImage>()
|
|
||||||
|
|
||||||
fileprivate var emulatorCoreSaveState: SaveStateProtocol?
|
fileprivate var emulatorCoreSaveState: SaveStateProtocol?
|
||||||
|
|
||||||
@ -76,11 +73,15 @@ class SaveStatesViewController: UICollectionViewController
|
|||||||
|
|
||||||
required init?(coder aDecoder: NSCoder)
|
required init?(coder aDecoder: NSCoder)
|
||||||
{
|
{
|
||||||
|
self.dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<SaveState, UIImage>(fetchedResultsController: NSFetchedResultsController())
|
||||||
|
|
||||||
self.dateFormatter = DateFormatter()
|
self.dateFormatter = DateFormatter()
|
||||||
self.dateFormatter.timeStyle = .short
|
self.dateFormatter.timeStyle = .short
|
||||||
self.dateFormatter.dateStyle = .short
|
self.dateFormatter.dateStyle = .short
|
||||||
|
|
||||||
super.init(coder: aDecoder)
|
super.init(coder: aDecoder)
|
||||||
|
|
||||||
|
self.prepareDataSource()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,21 +91,8 @@ extension SaveStatesViewController
|
|||||||
{
|
{
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
self.vibrancyView = UIVisualEffectView(effect: nil)
|
|
||||||
|
|
||||||
self.placeholderView = RSTPlaceholderView(frame: CGRect(x: 0, y: 0, width: self.vibrancyView.bounds.width, height: self.vibrancyView.bounds.height))
|
|
||||||
self.placeholderView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
||||||
self.placeholderView.textLabel.text = NSLocalizedString("No Save States", comment: "")
|
|
||||||
self.placeholderView.textLabel.textColor = UIColor.white
|
|
||||||
self.placeholderView.detailTextLabel.textColor = UIColor.white
|
|
||||||
self.vibrancyView.contentView.addSubview(self.placeholderView)
|
|
||||||
|
|
||||||
self.dataSource.proxy = self
|
|
||||||
self.dataSource.placeholderView = self.vibrancyView
|
|
||||||
self.dataSource.cellConfigurationHandler = { [unowned self] (cell, item, indexPath) in
|
|
||||||
self.configure(cell as! GridCollectionViewCell, for: indexPath)
|
|
||||||
}
|
|
||||||
self.collectionView?.dataSource = self.dataSource
|
self.collectionView?.dataSource = self.dataSource
|
||||||
|
self.collectionView?.prefetchDataSource = self.dataSource
|
||||||
|
|
||||||
let collectionViewLayout = self.collectionViewLayout as! GridCollectionViewLayout
|
let collectionViewLayout = self.collectionViewLayout as! GridCollectionViewLayout
|
||||||
let averageHorizontalInset = (collectionViewLayout.sectionInset.left + collectionViewLayout.sectionInset.right) / 2
|
let averageHorizontalInset = (collectionViewLayout.sectionInset.left + collectionViewLayout.sectionInset.right) / 2
|
||||||
@ -118,11 +106,11 @@ extension SaveStatesViewController
|
|||||||
{
|
{
|
||||||
case .saving:
|
case .saving:
|
||||||
self.title = NSLocalizedString("Save State", comment: "")
|
self.title = NSLocalizedString("Save State", comment: "")
|
||||||
placeholderView.detailTextLabel.text = NSLocalizedString("You can create a new save state by pressing the + button in the top right.", comment: "")
|
self.placeholderView.detailTextLabel.text = NSLocalizedString("You can create a new save state by pressing the + button in the top right.", comment: "")
|
||||||
|
|
||||||
case .loading:
|
case .loading:
|
||||||
self.title = NSLocalizedString("Load State", comment: "")
|
self.title = NSLocalizedString("Load State", comment: "")
|
||||||
placeholderView.detailTextLabel.text = NSLocalizedString("You can create a new save state by pressing the Save State option in the pause menu.", comment: "")
|
self.placeholderView.detailTextLabel.text = NSLocalizedString("You can create a new save state by pressing the Save State option in the pause menu.", comment: "")
|
||||||
self.navigationItem.rightBarButtonItem = nil
|
self.navigationItem.rightBarButtonItem = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,8 +146,55 @@ extension SaveStatesViewController
|
|||||||
|
|
||||||
private extension SaveStatesViewController
|
private extension SaveStatesViewController
|
||||||
{
|
{
|
||||||
//MARK: - Update -
|
func prepareDataSource()
|
||||||
|
{
|
||||||
|
self.dataSource.proxy = self
|
||||||
|
|
||||||
|
self.vibrancyView = UIVisualEffectView(effect: nil)
|
||||||
|
|
||||||
|
self.placeholderView = RSTPlaceholderView(frame: CGRect(x: 0, y: 0, width: self.vibrancyView.bounds.width, height: self.vibrancyView.bounds.height))
|
||||||
|
self.placeholderView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||||
|
self.placeholderView.textLabel.text = NSLocalizedString("No Save States", comment: "")
|
||||||
|
self.placeholderView.textLabel.textColor = UIColor.white
|
||||||
|
self.placeholderView.detailTextLabel.textColor = UIColor.white
|
||||||
|
self.vibrancyView.contentView.addSubview(self.placeholderView)
|
||||||
|
|
||||||
|
self.dataSource.placeholderView = self.vibrancyView
|
||||||
|
|
||||||
|
self.dataSource.cellConfigurationHandler = { [unowned self] (cell, item, indexPath) in
|
||||||
|
self.configure(cell as! GridCollectionViewCell, for: indexPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.dataSource.prefetchHandler = { [unowned self] (saveState, indexPath, completionHandler) in
|
||||||
|
let imageOperation = LoadImageURLOperation(url: saveState.imageFileURL)
|
||||||
|
imageOperation.resultHandler = { (image, error) in
|
||||||
|
completionHandler(image, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.isAppearing
|
||||||
|
{
|
||||||
|
imageOperation.start()
|
||||||
|
imageOperation.waitUntilFinished()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return imageOperation
|
||||||
|
}
|
||||||
|
|
||||||
|
self.dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
|
||||||
|
guard let image = image, let cell = cell as? GridCollectionViewCell else { return }
|
||||||
|
|
||||||
|
cell.imageView.backgroundColor = nil
|
||||||
|
cell.imageView.image = image
|
||||||
|
|
||||||
|
cell.isImageViewVibrancyEnabled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension SaveStatesViewController
|
||||||
|
{
|
||||||
|
//MARK: - Update -
|
||||||
func updateDataSource()
|
func updateDataSource()
|
||||||
{
|
{
|
||||||
let fetchRequest: NSFetchRequest<SaveState> = SaveState.fetchRequest()
|
let fetchRequest: NSFetchRequest<SaveState> = SaveState.fetchRequest()
|
||||||
@ -194,7 +229,7 @@ private extension SaveStatesViewController
|
|||||||
|
|
||||||
//MARK: - Configure Views -
|
//MARK: - Configure Views -
|
||||||
|
|
||||||
func configure(_ cell: GridCollectionViewCell, for indexPath: IndexPath, ignoreExpensiveOperations ignoreOperations: Bool = false)
|
func configure(_ cell: GridCollectionViewCell, for indexPath: IndexPath)
|
||||||
{
|
{
|
||||||
let saveState = self.dataSource.item(at: indexPath)
|
let saveState = self.dataSource.item(at: indexPath)
|
||||||
|
|
||||||
@ -213,32 +248,6 @@ private extension SaveStatesViewController
|
|||||||
cell.isImageViewVibrancyEnabled = true
|
cell.isImageViewVibrancyEnabled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if !ignoreOperations
|
|
||||||
{
|
|
||||||
let imageOperation = LoadImageURLOperation(url: saveState.imageFileURL)
|
|
||||||
imageOperation.resultsCache = self.imageCache
|
|
||||||
imageOperation.resultHandler = { (image, error) in
|
|
||||||
|
|
||||||
if let image = image
|
|
||||||
{
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
cell.imageView.backgroundColor = nil
|
|
||||||
cell.imageView.image = image
|
|
||||||
|
|
||||||
cell.isImageViewVibrancyEnabled = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure initially visible cells have loaded their image before they appear to prevent potential flickering from placeholder to thumbnail
|
|
||||||
if self.isAppearing
|
|
||||||
{
|
|
||||||
imageOperation.isImmediate = true
|
|
||||||
}
|
|
||||||
|
|
||||||
self.imageOperationQueue.addOperation(imageOperation, forKey: indexPath as NSCopying)
|
|
||||||
}
|
|
||||||
|
|
||||||
let deltaCore = Delta.core(for: self.game.type)!
|
let deltaCore = Delta.core(for: self.game.type)!
|
||||||
|
|
||||||
let dimensions = deltaCore.videoFormat.dimensions
|
let dimensions = deltaCore.videoFormat.dimensions
|
||||||
@ -577,7 +586,7 @@ extension SaveStatesViewController: UIViewControllerPreviewingDelegate
|
|||||||
|
|
||||||
let saveState = self.dataSource.item(at: indexPath)
|
let saveState = self.dataSource.item(at: indexPath)
|
||||||
let actions = self.actionsForSaveState(saveState)?.previewActions ?? []
|
let actions = self.actionsForSaveState(saveState)?.previewActions ?? []
|
||||||
let previewImage = self.imageCache.object(forKey: saveState.imageFileURL as NSURL) ?? UIImage(contentsOfFile: saveState.imageFileURL.path)
|
let previewImage = self.dataSource.prefetchItemCache.object(forKey: saveState) ?? UIImage(contentsOfFile: saveState.imageFileURL.path)
|
||||||
|
|
||||||
let previewGameViewController = PreviewGameViewController()
|
let previewGameViewController = PreviewGameViewController()
|
||||||
previewGameViewController.game = self.game
|
previewGameViewController.game = self.game
|
||||||
@ -655,12 +664,6 @@ extension SaveStatesViewController
|
|||||||
case .loading: self.loadSaveState(saveState)
|
case .loading: self.loadSaveState(saveState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath)
|
|
||||||
{
|
|
||||||
let operation = self.imageOperationQueue[indexPath as NSCopying]
|
|
||||||
operation?.cancel()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//MARK: - <UICollectionViewDelegateFlowLayout> -
|
//MARK: - <UICollectionViewDelegateFlowLayout> -
|
||||||
@ -668,8 +671,7 @@ extension SaveStatesViewController: UICollectionViewDelegateFlowLayout
|
|||||||
{
|
{
|
||||||
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
|
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
|
||||||
{
|
{
|
||||||
// No need to load images from disk just to determine size, so we pass true for ignoreExpensiveOperations
|
self.configure(self.prototypeCell, for: indexPath)
|
||||||
self.configure(self.prototypeCell, for: indexPath, ignoreExpensiveOperations: true)
|
|
||||||
|
|
||||||
let size = self.prototypeCell.contentView.systemLayoutSizeFitting(UILayoutFittingCompressedSize)
|
let size = self.prototypeCell.contentView.systemLayoutSizeFitting(UILayoutFittingCompressedSize)
|
||||||
return size
|
return size
|
||||||
|
|||||||
@ -12,15 +12,6 @@ import DeltaCore
|
|||||||
|
|
||||||
import Roxas
|
import Roxas
|
||||||
|
|
||||||
extension ControllerSkinsViewController
|
|
||||||
{
|
|
||||||
enum Section: Int
|
|
||||||
{
|
|
||||||
case standard
|
|
||||||
case custom
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ControllerSkinsViewController: UITableViewController
|
class ControllerSkinsViewController: UITableViewController
|
||||||
{
|
{
|
||||||
var system: System! {
|
var system: System! {
|
||||||
@ -35,11 +26,16 @@ class ControllerSkinsViewController: UITableViewController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate let dataSource = RSTFetchedResultsTableViewDataSource<ControllerSkin>(fetchedResultsController: NSFetchedResultsController())
|
fileprivate let dataSource: RSTFetchedResultsTableViewPrefetchingDataSource<ControllerSkin, UIImage>
|
||||||
|
|
||||||
fileprivate let imageOperationQueue = RSTOperationQueue()
|
required init?(coder aDecoder: NSCoder)
|
||||||
|
{
|
||||||
|
self.dataSource = RSTFetchedResultsTableViewPrefetchingDataSource<ControllerSkin, UIImage>(fetchedResultsController: NSFetchedResultsController())
|
||||||
|
|
||||||
fileprivate let imageCache = NSCache<ControllerSkinImageCacheKey, UIImage>()
|
super.init(coder: aDecoder)
|
||||||
|
|
||||||
|
self.prepareDataSource()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ControllerSkinsViewController
|
extension ControllerSkinsViewController
|
||||||
@ -48,11 +44,8 @@ extension ControllerSkinsViewController
|
|||||||
{
|
{
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
self.dataSource.proxy = self
|
|
||||||
self.dataSource.cellConfigurationHandler = { [unowned self] (cell, item, indexPath) in
|
|
||||||
self.configure(cell as! ControllerSkinTableViewCell, for: indexPath)
|
|
||||||
}
|
|
||||||
self.tableView.dataSource = self.dataSource
|
self.tableView.dataSource = self.dataSource
|
||||||
|
self.tableView.prefetchDataSource = self.dataSource
|
||||||
}
|
}
|
||||||
|
|
||||||
override func didReceiveMemoryWarning()
|
override func didReceiveMemoryWarning()
|
||||||
@ -65,6 +58,41 @@ extension ControllerSkinsViewController
|
|||||||
private extension ControllerSkinsViewController
|
private extension ControllerSkinsViewController
|
||||||
{
|
{
|
||||||
//MARK: - Update
|
//MARK: - Update
|
||||||
|
func prepareDataSource()
|
||||||
|
{
|
||||||
|
self.dataSource.proxy = self
|
||||||
|
self.dataSource.cellConfigurationHandler = { (cell, item, indexPath) in
|
||||||
|
let cell = cell as! ControllerSkinTableViewCell
|
||||||
|
|
||||||
|
cell.controllerSkinImageView.image = nil
|
||||||
|
cell.activityIndicatorView.startAnimating()
|
||||||
|
}
|
||||||
|
|
||||||
|
self.dataSource.prefetchHandler = { [unowned self] (controllerSkin, indexPath, completionHandler) in
|
||||||
|
let imageOperation = LoadControllerSkinImageOperation(controllerSkin: controllerSkin, traits: self.traits, size: UIScreen.main.defaultControllerSkinSize)
|
||||||
|
imageOperation.resultHandler = { (image, error) in
|
||||||
|
completionHandler(image, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure initially visible cells have loaded their image before they appear to prevent potential flickering from placeholder to thumbnail
|
||||||
|
if self.isAppearing
|
||||||
|
{
|
||||||
|
imageOperation.start()
|
||||||
|
imageOperation.waitUntilFinished()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return imageOperation
|
||||||
|
}
|
||||||
|
|
||||||
|
self.dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
|
||||||
|
guard let image = image, let cell = cell as? ControllerSkinTableViewCell else { return }
|
||||||
|
|
||||||
|
cell.controllerSkinImageView.image = image
|
||||||
|
cell.activityIndicatorView.stopAnimating()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func updateDataSource()
|
func updateDataSource()
|
||||||
{
|
{
|
||||||
guard let system = self.system, let traits = self.traits else { return }
|
guard let system = self.system, let traits = self.traits else { return }
|
||||||
@ -77,45 +105,6 @@ private extension ControllerSkinsViewController
|
|||||||
|
|
||||||
self.dataSource.fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext, sectionNameKeyPath: #keyPath(ControllerSkin.name), cacheName: nil)
|
self.dataSource.fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext, sectionNameKeyPath: #keyPath(ControllerSkin.name), cacheName: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
//MARK: - Configure Cells
|
|
||||||
func configure(_ cell: ControllerSkinTableViewCell, for indexPath: IndexPath)
|
|
||||||
{
|
|
||||||
let controllerSkin = self.dataSource.item(at: indexPath)
|
|
||||||
|
|
||||||
cell.controllerSkinImageView.image = nil
|
|
||||||
cell.activityIndicatorView.startAnimating()
|
|
||||||
|
|
||||||
let size = UIScreen.main.defaultControllerSkinSize
|
|
||||||
|
|
||||||
let imageOperation = LoadControllerSkinImageOperation(controllerSkin: controllerSkin, traits: self.traits, size: size)
|
|
||||||
imageOperation.resultsCache = self.imageCache
|
|
||||||
imageOperation.resultHandler = { (image, error) in
|
|
||||||
|
|
||||||
guard let image = image else { return }
|
|
||||||
|
|
||||||
if !imageOperation.isImmediate
|
|
||||||
{
|
|
||||||
UIView.transition(with: cell.controllerSkinImageView, duration: 0.2, options: .transitionCrossDissolve, animations: {
|
|
||||||
cell.controllerSkinImageView.image = image
|
|
||||||
}, completion: nil)
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
cell.controllerSkinImageView.image = image
|
|
||||||
}
|
|
||||||
|
|
||||||
cell.activityIndicatorView.stopAnimating()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure initially visible cells have loaded their image before they appear to prevent potential flickering from placeholder to thumbnail
|
|
||||||
if self.isAppearing
|
|
||||||
{
|
|
||||||
imageOperation.isImmediate = true
|
|
||||||
}
|
|
||||||
|
|
||||||
self.imageOperationQueue.addOperation(imageOperation, forKey: indexPath as NSCopying)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ControllerSkinsViewController
|
extension ControllerSkinsViewController
|
||||||
@ -127,33 +116,6 @@ extension ControllerSkinsViewController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ControllerSkinsViewController: UITableViewDataSourcePrefetching
|
|
||||||
{
|
|
||||||
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath])
|
|
||||||
{
|
|
||||||
for indexPath in indexPaths
|
|
||||||
{
|
|
||||||
let controllerSkin = self.dataSource.item(at: indexPath)
|
|
||||||
|
|
||||||
let size = UIScreen.main.defaultControllerSkinSize
|
|
||||||
|
|
||||||
let imageOperation = LoadControllerSkinImageOperation(controllerSkin: controllerSkin, traits: self.traits, size: size)
|
|
||||||
imageOperation.resultsCache = self.imageCache
|
|
||||||
|
|
||||||
self.imageOperationQueue.addOperation(imageOperation, forKey: indexPath as NSCopying)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath])
|
|
||||||
{
|
|
||||||
for indexPath in indexPaths
|
|
||||||
{
|
|
||||||
let operation = self.imageOperationQueue[indexPath as NSCopying]
|
|
||||||
operation?.cancel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension ControllerSkinsViewController
|
extension ControllerSkinsViewController
|
||||||
{
|
{
|
||||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
|
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
|
||||||
@ -176,10 +138,4 @@ extension ControllerSkinsViewController
|
|||||||
|
|
||||||
return height
|
return height
|
||||||
}
|
}
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath)
|
|
||||||
{
|
|
||||||
let operation = self.imageOperationQueue[indexPath as NSCopying]
|
|
||||||
operation?.cancel()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
2
External/Roxas
vendored
2
External/Roxas
vendored
@ -1 +1 @@
|
|||||||
Subproject commit 3ecd5e6e727181d0d1c9984079a809483c247c24
|
Subproject commit 7434aef0372aca1d0b12cc4b8a6a37df034aae7c
|
||||||
Loading…
Reference in New Issue
Block a user