I use the most usual method to load images:
dataSource = DataSource(collectionView: collectionView) { collectionView, indexPath, identifier in
switch indexPath.section {
case 0:
return self.configureCell(type: DSCVFeed1CollectionViewCell.self, in: collectionView, for: indexPath) { cell in
guard case let .feed1(model) = identifier else { return }
cell.representedId = model.identifier
cell.configure(with: model)
self.asyncFetcher.fetchAsync(
for: model.identifier,
with: (model.imageUrl, cell.illustrationSize, collectionView.traitCollection.displayScale)
) { image in
DispatchQueue.main.async {
guard cell.representedId == model.identifier else { return }
cell.configure(with: image)
}
}
}
// ...
and
func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths {
guard indexPath.section != 10 else { continue }
guard let item = dataSource.itemIdentifier(for: indexPath) else { continue }
asyncFetcher.fetchAsync(
for: item.identifier,
with: (item.imageUrl, CGSize(width: 100.0, height: 100.0), collectionView.traitCollection.displayScale)
)
}
}
func collectionView(_ collectionView: UICollectionView, cancelPrefetchingForItemsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths {
guard indexPath.section != 10 else { continue }
guard let item = dataSource.itemIdentifier(for: indexPath) else { continue }
asyncFetcher.cancelFetch(item.identifier)
}
}
Also I use simple AsyncImageFetcher(with some custom) class provided by Apple in UICollectionViewDataSourcePrefetching documentation:
final class AsyncImageFetcher {
// MARK: - OSLog
private static let log =
ProcessInfo.processInfo.environment.keys.contains("SIGNPOSTS_FOR_ASYNCIMAGEFETCHER")
? OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "AsyncImageFetcher")
: .disabled
// MARK: - Queues
private let underlyingQueue: DispatchQueue?
private let fetchQueue: PSOperationQueue
// MARK: - Stores
private var operations = [UUID: GetPreferredImageOperation]()
typealias CompletionHandler = (UIImage) -> Void
private var completionHandlers = [UUID: [CompletionHandler]]()
private let cache = PSCache<uuid, uiimage="">()
// MARK: - DI
typealias Dependency = (URLSession)
let (session): Dependency
// MARK: - Initializers
init(dependency: Dependency = (.shared), underlineQueue: DispatchQueue? = nil) {
(session) = dependency
self.underlyingQueue = underlineQueue
fetchQueue = PSOperationQueue()
fetchQueue.underlyingQueue = underlineQueue
}
// MARK: - Fetch API
typealias Parameters = (url: URL, pointSize: CGSize, scaleFactor: CGFloat)
func fetchAsync(
for identifier: UUID,
with params: Parameters,
completionHandler: CompletionHandler? = nil
) {
if let completionHandler = completionHandler {
let completionHandlers = self.completionHandlers[identifier, default: []]
self.completionHandlers[identifier] = completionHandlers + CollectionOfOne(completionHandler)
}
self.fetchData(for: identifier, with: params)
}
func cancelFetch(_ identifier: UUID) {
//self.fetchQueue.isSuspended = true
//defer { self.fetchQueue.isSuspended = false }
// self.operations[identifier]?.cancel()
// self.completionHandlers[identifier] = nil
}
// MARK: - Private
private func fetchData(for identifier: UUID, with params: Parameters) {
print(operations.count)
guard operations[identifier] == nil else { return }
if let data = cache[identifier] {
invokeCompletionHandlers(for: identifier, with: data)
} else {
let operation =
GetPreferredImageOperation(
identifier: identifier,
underlyingQueue: underlyingQueue,
session: session,
at: params.url,
to: params.pointSize,
withScale: params.scaleFactor
) { [weak self] (preferredImage) in
guard let image = preferredImage else { return }
self?.cache[identifier] = image
self?.invokeCompletionHandlers(for: identifier, with: image)
}
operation.completionBlock = { [weak self, unowned operation] in
DispatchQueue.main.async {
self?.operations[operation.identifier] = nil
}
}
operations[identifier] = operation
fetchQueue.addOperation(operation)
}
}
private func invokeCompletionHandlers(for identifier: UUID, with fetchedData: UIImage) {
let completionHandlers = self.completionHandlers[identifier, default: []]
self.completionHandlers[identifier] = nil
for completionHandler in completionHandlers { completionHandler(fetchedData) }
}
}
GetPreferredImageOperation is a simpleGroupOperationfrom AdvancedOperation WWDC topic.https://gist.github.com/jeudesprits/39c1e97bfda4e7a7b739c2deb50b9c3b
https://gist.github.com/jeudesprits/8814279336d38de8167fd2426a63e995
The problem is that without canceling operations, everything works much faster and creates much fewer threads. Although it should be exactly the opposite. What could be the matter? All OperationQueue`s use a same private serial queue as underlyingQueue.
Without cancelling:
8i3t26.jpg
With cancelation:
LfeKZa.jpg