diff --git a/Delta/Components/Loading/LoadImageURLOperation.swift b/Delta/Components/Loading/LoadImageURLOperation.swift index 97036a6..c05a9df 100644 --- a/Delta/Components/Loading/LoadImageURLOperation.swift +++ b/Delta/Components/Loading/LoadImageURLOperation.swift @@ -45,6 +45,11 @@ class LoadImageURLOperation: RSTLoadOperation super.cancel() self.downloadOperation?.cancel() + + if self.isAsynchronous + { + self.finish() + } } override func loadResult(completion: @escaping (UIImage?, Swift.Error?) -> Void) diff --git a/Delta/Database/OpenVGDB/GamesDatabaseBrowserViewController.swift b/Delta/Database/OpenVGDB/GamesDatabaseBrowserViewController.swift index 1945403..c201e10 100644 --- a/Delta/Database/OpenVGDB/GamesDatabaseBrowserViewController.swift +++ b/Delta/Database/OpenVGDB/GamesDatabaseBrowserViewController.swift @@ -16,10 +16,8 @@ class GamesDatabaseBrowserViewController: UITableViewController var selectionHandler: ((GameMetadata) -> Void)? fileprivate let database: GamesDatabase? - fileprivate let dataSource: RSTArrayTableViewDataSource - fileprivate let operationQueue = RSTOperationQueue() - fileprivate let imageCache = NSCache() + fileprivate let dataSource: RSTArrayTableViewPrefetchingDataSource override init(style: UITableViewStyle) { fatalError() @@ -37,39 +35,13 @@ class GamesDatabaseBrowserViewController: UITableViewController print(error) } - self.dataSource = RSTArrayTableViewDataSource(items: []) - - let placeholderView = RSTPlaceholderView() - placeholderView.textLabel.textColor = UIColor.lightText - placeholderView.detailTextLabel.textColor = UIColor.lightText - - self.dataSource.placeholderView = placeholderView + self.dataSource = RSTArrayTableViewPrefetchingDataSource(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() } } diff --git a/Delta/Game Selection/GameCollectionViewController.swift b/Delta/Game Selection/GameCollectionViewController.swift index d43bea2..5608ee9 100644 --- a/Delta/Game Selection/GameCollectionViewController.swift +++ b/Delta/Game Selection/GameCollectionViewController.swift @@ -45,17 +45,23 @@ class GameCollectionViewController: UICollectionViewController fileprivate var activeSaveState: SaveStateProtocol? - fileprivate let dataSource = RSTFetchedResultsCollectionViewDataSource(fetchedResultsController: NSFetchedResultsController()) + fileprivate let dataSource: RSTFetchedResultsCollectionViewPrefetchingDataSource fileprivate let prototypeCell = GridCollectionViewCell() - fileprivate let imageOperationQueue = RSTOperationQueue() - fileprivate let imageCache = NSCache() - 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(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.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 diff --git a/Delta/Pause Menu/Save States/SaveStatesViewController.swift b/Delta/Pause Menu/Save States/SaveStatesViewController.swift index 4844fed..d9048a6 100644 --- a/Delta/Pause Menu/Save States/SaveStatesViewController.swift +++ b/Delta/Pause Menu/Save States/SaveStatesViewController.swift @@ -65,10 +65,7 @@ class SaveStatesViewController: UICollectionViewController fileprivate var prototypeCellWidthConstraint: NSLayoutConstraint! fileprivate var prototypeHeader = SaveStatesCollectionHeaderView() - fileprivate let dataSource = RSTFetchedResultsCollectionViewDataSource(fetchedResultsController: NSFetchedResultsController()) - - fileprivate let imageOperationQueue = RSTOperationQueue() - fileprivate let imageCache = NSCache() + fileprivate let dataSource: RSTFetchedResultsCollectionViewPrefetchingDataSource fileprivate var emulatorCoreSaveState: SaveStateProtocol? @@ -76,11 +73,15 @@ class SaveStatesViewController: UICollectionViewController required init?(coder aDecoder: NSCoder) { + self.dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource(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.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: - - @@ -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 diff --git a/Delta/Settings/Controller Skins/ControllerSkinsViewController.swift b/Delta/Settings/Controller Skins/ControllerSkinsViewController.swift index 25bf6d7..f5835a2 100644 --- a/Delta/Settings/Controller Skins/ControllerSkinsViewController.swift +++ b/Delta/Settings/Controller Skins/ControllerSkinsViewController.swift @@ -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(fetchedResultsController: NSFetchedResultsController()) + fileprivate let dataSource: RSTFetchedResultsTableViewPrefetchingDataSource - fileprivate let imageOperationQueue = RSTOperationQueue() - - fileprivate let imageCache = NSCache() + required init?(coder aDecoder: NSCoder) + { + self.dataSource = RSTFetchedResultsTableViewPrefetchingDataSource(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() - } } diff --git a/External/Roxas b/External/Roxas index 3ecd5e6..7434aef 160000 --- a/External/Roxas +++ b/External/Roxas @@ -1 +1 @@ -Subproject commit 3ecd5e6e727181d0d1c9984079a809483c247c24 +Subproject commit 7434aef0372aca1d0b12cc4b8a6a37df034aae7c