-
Notifications
You must be signed in to change notification settings - Fork 30
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adds the ability to integrate with collection/table view prefetch
Prefetch operations are requested from CellActions and executed in a private OperationQueue. The actual fetching/cancelling is delayed by a short period of time because iOS may trigger rapid fetch/cancel operations and this is an attempt to reduce the amount of fetch/cancel/fetch of the same IndexPath
- Loading branch information
Showing
7 changed files
with
288 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
// | ||
// FunctionalTableData+UITableViewDataSourcePrefetching.swift | ||
// FunctionalTableData | ||
// | ||
// Created by Geoffrey Foster on 2019-03-25. | ||
// Copyright © 2019 Shopify. All rights reserved. | ||
// | ||
|
||
import Foundation | ||
|
||
/// A custom implementation of the `UITableViewDataSourcePrefetching` and `UICollectionViewDataSourcePrefetching` protocols that hooks into the `CellAction` type and uses its `prefetchAction: PrefetchAction?` | ||
class DataSourcePrefetching: NSObject, UITableViewDataSourcePrefetching, UICollectionViewDataSourcePrefetching { | ||
private struct PrefetchOperationWrapper { | ||
let observations: (finished: NSKeyValueObservation, cancelled: NSKeyValueObservation) | ||
let operation: Operation | ||
|
||
init(operation: Operation, observations: (finished: NSKeyValueObservation, cancelled: NSKeyValueObservation)) { | ||
self.operation = operation | ||
self.observations = observations | ||
} | ||
} | ||
|
||
private let queue = OperationQueue() | ||
|
||
private var operations: [ItemPath: PrefetchOperationWrapper] = [:] | ||
private var prefetched: Set<ItemPath> = [] | ||
private let operationsLock = NSLock() | ||
|
||
private var operationItemPathsToPrefetch: Set<ItemPath> = [] | ||
private var operationItemPathsToCancel: Set<ItemPath> = [] | ||
|
||
var sections: [TableSection] = [] | ||
|
||
private let delay: (prefetch: TimeInterval, cancel: TimeInterval) | ||
|
||
init(delay: (prefetch: TimeInterval, cancel: TimeInterval) = (0.1, 0.1)) { | ||
self.delay = delay | ||
} | ||
|
||
// MARK: - UITableViewDataSourcePrefetching | ||
|
||
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { | ||
perform(.prefetch, indexPaths: indexPaths) | ||
} | ||
|
||
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) { | ||
perform(.cancel, indexPaths: indexPaths) | ||
} | ||
|
||
// MARK: - UICollectionViewDataSourcePrefetching | ||
|
||
func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) { | ||
perform(.prefetch, indexPaths: indexPaths) | ||
} | ||
|
||
func collectionView(_ collectionView: UICollectionView, cancelPrefetchingForItemsAt indexPaths: [IndexPath]) { | ||
perform(.cancel, indexPaths: indexPaths) | ||
} | ||
|
||
// MARK: - | ||
|
||
private enum PrefetchOperationKind { | ||
case prefetch | ||
case cancel | ||
} | ||
|
||
private func perform(_ kind: PrefetchOperationKind, indexPaths: [IndexPath]) { | ||
// Because we can get rapid fire cancel/prefetch calls that contain the same IndexPath values we queue up ones to be cancelled and delay the cancel and queue up the fetches and delay the fetch | ||
let itemPaths = sections.itemPaths(from: indexPaths) | ||
|
||
let performDelay: TimeInterval | ||
switch kind { | ||
case .prefetch: | ||
operationItemPathsToPrefetch.formUnion(itemPaths) | ||
operationItemPathsToCancel.subtract(itemPaths) | ||
performDelay = delay.prefetch | ||
case .cancel: | ||
operationItemPathsToCancel.formUnion(itemPaths) | ||
operationItemPathsToPrefetch.subtract(itemPaths) | ||
performDelay = delay.cancel | ||
} | ||
|
||
NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(run), object: nil) | ||
perform(#selector(run), with: nil, afterDelay: performDelay) | ||
} | ||
|
||
private func registerOperation(_ operation: Operation, forItemPath itemPath: ItemPath) -> Operation { | ||
func operationFinished(for itemPath: ItemPath, successfully: Bool) { | ||
operationsLock.withLock { | ||
_ = operations.removeValue(forKey: itemPath) | ||
if successfully { | ||
prefetched.insert(itemPath) | ||
} | ||
} | ||
} | ||
|
||
let finishedObservation = operation.observe(\.isFinished, options: .new) { (operation, change) in | ||
if change.newValue == true { | ||
operationFinished(for: itemPath, successfully: true) | ||
} | ||
} | ||
let cancelledObservation = operation.observe(\.isCancelled, options: .new) { (operation, change) in | ||
if change.newValue == true { | ||
operationFinished(for: itemPath, successfully: false) | ||
} | ||
} | ||
let prefetchOperation = PrefetchOperationWrapper(operation: operation, observations: (finished: finishedObservation, cancelled: cancelledObservation)) | ||
|
||
operationsLock.withLock { | ||
operations[itemPath] = prefetchOperation | ||
} | ||
|
||
return operation | ||
} | ||
|
||
@objc private func run() { | ||
/// Returns `true` if and only if an operation for the given `ItemPath` doesn't already exist and also hasn't already been run. | ||
/// | ||
/// - Parameter itemPath: | ||
/// - Returns: `true` if a prefetch operation should be created and run for the given `ItemPath`. | ||
func shouldPrefetch(itemPath: ItemPath) -> Bool { | ||
return operationsLock.withLock { | ||
operations[itemPath] == nil && !prefetched.contains(itemPath) | ||
} | ||
} | ||
|
||
var newOperations: [Operation] = [] | ||
for itemPath in operationItemPathsToPrefetch where shouldPrefetch(itemPath: itemPath) { | ||
guard let prefetchAction = sections[itemPath]?.actions.prefetchAction else { continue } | ||
let operation = prefetchAction() | ||
newOperations.append(registerOperation(operation, forItemPath: itemPath)) | ||
} | ||
|
||
let cancelledOperations = operationsLock.withLock { | ||
operations.filter { operationItemPathsToCancel.contains($0.key) }.map { $0.value } | ||
} | ||
|
||
operationItemPathsToPrefetch.removeAll() | ||
operationItemPathsToCancel.removeAll() | ||
|
||
queue.addOperations(newOperations, waitUntilFinished: false) | ||
cancelledOperations.forEach { $0.operation.cancel() } | ||
} | ||
} | ||
|
||
private extension NSLocking { | ||
func withLock<T> (_ body: () throws -> T) rethrows -> T { | ||
self.lock() | ||
defer { self.unlock() } | ||
return try body() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.