Refactors UICollectionViewControllers/UITableViewControllers to use prefetching RSTCellContentDataSources

This commit is contained in:
Riley Testut 2017-07-07 21:58:29 -05:00
parent 0c567de380
commit 812a773fba
6 changed files with 233 additions and 253 deletions

View File

@ -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)

View File

@ -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)
} }
} }

View File

@ -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,12 +544,9 @@ 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
temporaryGame.artworkURL = imageURL temporaryGame.artworkURL = imageURL
@ -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

View File

@ -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
} }
@ -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 private extension SaveStatesViewController
{ {
//MARK: - Update - //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

View File

@ -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)
{
fileprivate let imageCache = NSCache<ControllerSkinImageCacheKey, UIImage>() self.dataSource = RSTFetchedResultsTableViewPrefetchingDataSource<ControllerSkin, UIImage>(fetchedResultsController: NSFetchedResultsController())
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

@ -1 +1 @@
Subproject commit 3ecd5e6e727181d0d1c9984079a809483c247c24 Subproject commit 7434aef0372aca1d0b12cc4b8a6a37df034aae7c