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()
|
||||
|
||||
self.downloadOperation?.cancel()
|
||||
|
||||
if self.isAsynchronous
|
||||
{
|
||||
self.finish()
|
||||
}
|
||||
}
|
||||
|
||||
override func loadResult(completion: @escaping (UIImage?, Swift.Error?) -> Void)
|
||||
|
||||
@ -16,10 +16,8 @@ class GamesDatabaseBrowserViewController: UITableViewController
|
||||
var selectionHandler: ((GameMetadata) -> Void)?
|
||||
|
||||
fileprivate let database: GamesDatabase?
|
||||
fileprivate let dataSource: RSTArrayTableViewDataSource<GameMetadata>
|
||||
|
||||
fileprivate let operationQueue = RSTOperationQueue()
|
||||
fileprivate let imageCache = NSCache<NSURL, UIImage>()
|
||||
fileprivate let dataSource: RSTArrayTableViewPrefetchingDataSource<GameMetadata, UIImage>
|
||||
|
||||
override init(style: UITableViewStyle) {
|
||||
fatalError()
|
||||
@ -37,39 +35,13 @@ class GamesDatabaseBrowserViewController: UITableViewController
|
||||
print(error)
|
||||
}
|
||||
|
||||
self.dataSource = RSTArrayTableViewDataSource<GameMetadata>(items: [])
|
||||
|
||||
let placeholderView = RSTPlaceholderView()
|
||||
placeholderView.textLabel.textColor = UIColor.lightText
|
||||
placeholderView.detailTextLabel.textColor = UIColor.lightText
|
||||
|
||||
self.dataSource.placeholderView = placeholderView
|
||||
self.dataSource = RSTArrayTableViewPrefetchingDataSource<GameMetadata, UIImage>(items: [])
|
||||
|
||||
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.prepareDataSource()
|
||||
}
|
||||
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||
@ -83,6 +55,8 @@ class GamesDatabaseBrowserViewController: UITableViewController
|
||||
self.view.backgroundColor = UIColor.deltaDarkGray
|
||||
|
||||
self.tableView.dataSource = self.dataSource
|
||||
self.tableView.prefetchDataSource = self.dataSource
|
||||
|
||||
self.tableView.indicatorStyle = .white
|
||||
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)
|
||||
{
|
||||
@ -112,30 +152,6 @@ extension GamesDatabaseBrowserViewController
|
||||
cell.artworkImageViewTrailingConstraint.constant = 15
|
||||
|
||||
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()
|
||||
@ -153,6 +169,12 @@ extension GamesDatabaseBrowserViewController
|
||||
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
|
||||
@ -167,12 +189,6 @@ extension GamesDatabaseBrowserViewController
|
||||
let metadata = self.dataSource.item(at: indexPath)
|
||||
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
|
||||
@ -193,7 +209,6 @@ extension GamesDatabaseBrowserViewController: UISearchControllerDelegate
|
||||
func didDismissSearchController(_ searchController: UISearchController)
|
||||
{
|
||||
// Fix potentially incorrect offset if user dismisses searchController while scrolling
|
||||
self.tableView.setContentOffset(CGPoint.zero, animated: false)
|
||||
self.tableView.setContentOffset(CGPoint(x: 0, y: -self.topLayoutGuide.length), animated: false)
|
||||
self.resetTableViewContentOffset()
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,17 +45,23 @@ class GameCollectionViewController: UICollectionViewController
|
||||
|
||||
fileprivate var activeSaveState: SaveStateProtocol?
|
||||
|
||||
fileprivate let dataSource = RSTFetchedResultsCollectionViewDataSource<Game>(fetchedResultsController: NSFetchedResultsController())
|
||||
fileprivate let dataSource: RSTFetchedResultsCollectionViewPrefetchingDataSource<Game, UIImage>
|
||||
fileprivate let prototypeCell = GridCollectionViewCell()
|
||||
|
||||
fileprivate let imageOperationQueue = RSTOperationQueue()
|
||||
fileprivate let imageCache = NSCache<NSURL, UIImage>()
|
||||
|
||||
fileprivate var _performing3DTouchTransition = false
|
||||
fileprivate weak var _destination3DTouchTransitionViewController: UIViewController?
|
||||
|
||||
fileprivate var _renameAction: UIAlertAction?
|
||||
fileprivate var _changingArtworkGame: Game?
|
||||
|
||||
required init?(coder aDecoder: NSCoder)
|
||||
{
|
||||
self.dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<Game, UIImage>(fetchedResultsController: NSFetchedResultsController())
|
||||
|
||||
super.init(coder: aDecoder)
|
||||
|
||||
self.prepareDataSource()
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - UIViewController -
|
||||
@ -66,11 +72,8 @@ extension GameCollectionViewController
|
||||
{
|
||||
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?.prefetchDataSource = self.dataSource
|
||||
self.collectionView?.delegate = self
|
||||
|
||||
let layout = self.collectionViewLayout as! GridCollectionViewLayout
|
||||
@ -182,7 +185,33 @@ extension GameCollectionViewController
|
||||
//MARK: - Private Methods -
|
||||
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()
|
||||
{
|
||||
let fetchRequest: NSFetchRequest<Game> = Game.fetchRequest()
|
||||
@ -194,7 +223,7 @@ private extension GameCollectionViewController
|
||||
}
|
||||
|
||||
//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)
|
||||
|
||||
@ -214,24 +243,6 @@ private extension GameCollectionViewController
|
||||
cell.maximumImageSize = CGSize(width: 90, height: 90)
|
||||
cell.textLabel.text = game.name
|
||||
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
|
||||
@ -533,12 +544,9 @@ extension GameCollectionViewController: ImportControllerDelegate
|
||||
|
||||
if let imageURL = imageURL
|
||||
{
|
||||
if let previousArtworkURL = game.artworkURL as NSURL?
|
||||
{
|
||||
// Remove previous artwork from cache.
|
||||
self.imageCache.removeObject(forKey: previousArtworkURL)
|
||||
}
|
||||
|
||||
// Remove previous artwork from cache.
|
||||
self.dataSource.prefetchItemCache.removeObject(forKey: game)
|
||||
|
||||
DatabaseManager.shared.performBackgroundTask { (context) in
|
||||
let temporaryGame = context.object(with: game.objectID) as! Game
|
||||
temporaryGame.artworkURL = imageURL
|
||||
@ -626,12 +634,6 @@ extension GameCollectionViewController
|
||||
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 -
|
||||
@ -646,7 +648,7 @@ extension GameCollectionViewController: UICollectionViewDelegateFlowLayout
|
||||
widthConstraint.isActive = true
|
||||
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)
|
||||
return size
|
||||
|
||||
@ -65,10 +65,7 @@ class SaveStatesViewController: UICollectionViewController
|
||||
fileprivate var prototypeCellWidthConstraint: NSLayoutConstraint!
|
||||
fileprivate var prototypeHeader = SaveStatesCollectionHeaderView()
|
||||
|
||||
fileprivate let dataSource = RSTFetchedResultsCollectionViewDataSource<SaveState>(fetchedResultsController: NSFetchedResultsController())
|
||||
|
||||
fileprivate let imageOperationQueue = RSTOperationQueue()
|
||||
fileprivate let imageCache = NSCache<NSURL, UIImage>()
|
||||
fileprivate let dataSource: RSTFetchedResultsCollectionViewPrefetchingDataSource<SaveState, UIImage>
|
||||
|
||||
fileprivate var emulatorCoreSaveState: SaveStateProtocol?
|
||||
|
||||
@ -76,11 +73,15 @@ class SaveStatesViewController: UICollectionViewController
|
||||
|
||||
required init?(coder aDecoder: NSCoder)
|
||||
{
|
||||
self.dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<SaveState, UIImage>(fetchedResultsController: NSFetchedResultsController())
|
||||
|
||||
self.dateFormatter = DateFormatter()
|
||||
self.dateFormatter.timeStyle = .short
|
||||
self.dateFormatter.dateStyle = .short
|
||||
|
||||
super.init(coder: aDecoder)
|
||||
|
||||
self.prepareDataSource()
|
||||
}
|
||||
}
|
||||
|
||||
@ -90,21 +91,8 @@ extension SaveStatesViewController
|
||||
{
|
||||
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?.prefetchDataSource = self.dataSource
|
||||
|
||||
let collectionViewLayout = self.collectionViewLayout as! GridCollectionViewLayout
|
||||
let averageHorizontalInset = (collectionViewLayout.sectionInset.left + collectionViewLayout.sectionInset.right) / 2
|
||||
@ -118,11 +106,11 @@ extension SaveStatesViewController
|
||||
{
|
||||
case .saving:
|
||||
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:
|
||||
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
|
||||
}
|
||||
|
||||
@ -156,10 +144,57 @@ extension SaveStatesViewController
|
||||
}
|
||||
}
|
||||
|
||||
private extension SaveStatesViewController
|
||||
{
|
||||
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()
|
||||
{
|
||||
let fetchRequest: NSFetchRequest<SaveState> = SaveState.fetchRequest()
|
||||
@ -194,7 +229,7 @@ private extension SaveStatesViewController
|
||||
|
||||
//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)
|
||||
|
||||
@ -213,32 +248,6 @@ private extension SaveStatesViewController
|
||||
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 dimensions = deltaCore.videoFormat.dimensions
|
||||
@ -577,7 +586,7 @@ extension SaveStatesViewController: UIViewControllerPreviewingDelegate
|
||||
|
||||
let saveState = self.dataSource.item(at: indexPath)
|
||||
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()
|
||||
previewGameViewController.game = self.game
|
||||
@ -655,12 +664,6 @@ extension SaveStatesViewController
|
||||
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> -
|
||||
@ -668,8 +671,7 @@ extension SaveStatesViewController: UICollectionViewDelegateFlowLayout
|
||||
{
|
||||
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, ignoreExpensiveOperations: true)
|
||||
self.configure(self.prototypeCell, for: indexPath)
|
||||
|
||||
let size = self.prototypeCell.contentView.systemLayoutSizeFitting(UILayoutFittingCompressedSize)
|
||||
return size
|
||||
|
||||
@ -12,15 +12,6 @@ import DeltaCore
|
||||
|
||||
import Roxas
|
||||
|
||||
extension ControllerSkinsViewController
|
||||
{
|
||||
enum Section: Int
|
||||
{
|
||||
case standard
|
||||
case custom
|
||||
}
|
||||
}
|
||||
|
||||
class ControllerSkinsViewController: UITableViewController
|
||||
{
|
||||
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()
|
||||
|
||||
fileprivate let imageCache = NSCache<ControllerSkinImageCacheKey, UIImage>()
|
||||
required init?(coder aDecoder: NSCoder)
|
||||
{
|
||||
self.dataSource = RSTFetchedResultsTableViewPrefetchingDataSource<ControllerSkin, UIImage>(fetchedResultsController: NSFetchedResultsController())
|
||||
|
||||
super.init(coder: aDecoder)
|
||||
|
||||
self.prepareDataSource()
|
||||
}
|
||||
}
|
||||
|
||||
extension ControllerSkinsViewController
|
||||
@ -48,11 +44,8 @@ extension ControllerSkinsViewController
|
||||
{
|
||||
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.prefetchDataSource = self.dataSource
|
||||
}
|
||||
|
||||
override func didReceiveMemoryWarning()
|
||||
@ -65,6 +58,41 @@ extension ControllerSkinsViewController
|
||||
private extension ControllerSkinsViewController
|
||||
{
|
||||
//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()
|
||||
{
|
||||
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)
|
||||
}
|
||||
|
||||
//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
|
||||
@ -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
|
||||
{
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
|
||||
@ -176,10 +138,4 @@ extension ControllerSkinsViewController
|
||||
|
||||
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