Ditch custom image request grouping, rely on URLSession's
This commit is contained in:
parent
ba032412eb
commit
59d866aa23
|
@ -24,10 +24,6 @@ class ImageCache {
|
||||||
private let cache: ImageDataCache
|
private let cache: ImageDataCache
|
||||||
private let desiredPixelSize: CGSize?
|
private let desiredPixelSize: CGSize?
|
||||||
|
|
||||||
private var groups = MultiThreadDictionary<URL, RequestGroup>()
|
|
||||||
|
|
||||||
private var backgroundQueue = DispatchQueue(label: "ImageCache completion queue", qos: .default)
|
|
||||||
|
|
||||||
init(name: String, memoryExpiry: CacheExpiry, diskExpiry: CacheExpiry? = nil, desiredSize: CGSize? = nil) {
|
init(name: String, memoryExpiry: CacheExpiry, diskExpiry: CacheExpiry? = nil, desiredSize: CGSize? = nil) {
|
||||||
// todo: might not always want to use UIScreen.main for this, e.g. Catalyst?
|
// todo: might not always want to use UIScreen.main for this, e.g. Catalyst?
|
||||||
let pixelSize = desiredSize?.applying(.init(scaleX: UIScreen.main.scale, y: UIScreen.main.scale))
|
let pixelSize = desiredSize?.applying(.init(scaleX: UIScreen.main.scale, y: UIScreen.main.scale))
|
||||||
|
@ -61,14 +57,9 @@ class ImageCache {
|
||||||
wrappedCompletion?(entry.data, entry.image)
|
wrappedCompletion?(entry.data, entry.image)
|
||||||
return nil
|
return nil
|
||||||
} else {
|
} else {
|
||||||
if let group = groups[url] {
|
let task = dataTask(url: url, completion: wrappedCompletion)
|
||||||
return group.addCallback(wrappedCompletion)
|
task.resume()
|
||||||
} else {
|
return task
|
||||||
let group = createGroup(url: url)
|
|
||||||
let request = group.addCallback(wrappedCompletion)
|
|
||||||
group.run()
|
|
||||||
return request
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,22 +76,23 @@ class ImageCache {
|
||||||
// if caching is disabled, don't bother fetching since nothing will be done with the result
|
// if caching is disabled, don't bother fetching since nothing will be done with the result
|
||||||
guard !ImageCache.disableCaching else { return }
|
guard !ImageCache.disableCaching else { return }
|
||||||
|
|
||||||
if !((try? cache.has(url.absoluteString)) ?? false),
|
if !((try? cache.has(url.absoluteString)) ?? false) {
|
||||||
!groups.contains(key: url) {
|
let task = dataTask(url: url) { data, image in
|
||||||
let group = createGroup(url: url)
|
guard let data else { return }
|
||||||
group.run()
|
try? self.cache.set(url.absoluteString, data: data, image: image)
|
||||||
|
}
|
||||||
|
task.resume()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func createGroup(url: URL) -> RequestGroup {
|
private func dataTask(url: URL, completion: ((Data?, UIImage?) -> Void)?) -> URLSessionDataTask {
|
||||||
let group = RequestGroup(url: url) { (data, image) in
|
return URLSession.shared.dataTask(with: url) { data, response, error in
|
||||||
if let data = data {
|
guard error == nil,
|
||||||
try? self.cache.set(url.absoluteString, data: data, image: image)
|
let data else {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
_ = self.groups.removeValue(forKey: url)
|
completion?(data, UIImage(data: data))
|
||||||
}
|
}
|
||||||
groups[url] = group
|
|
||||||
return group
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getData(_ url: URL) -> Data? {
|
func getData(_ url: URL) -> Data? {
|
||||||
|
@ -111,87 +103,10 @@ class ImageCache {
|
||||||
return try? cache.get(url.absoluteString, loadOriginal: loadOriginal)
|
return try? cache.get(url.absoluteString, loadOriginal: loadOriginal)
|
||||||
}
|
}
|
||||||
|
|
||||||
func cancelWithoutCallback(_ url: URL) {
|
|
||||||
groups[url]?.cancelWithoutCallback()
|
|
||||||
}
|
|
||||||
|
|
||||||
func reset() throws {
|
func reset() throws {
|
||||||
try cache.removeAll()
|
try cache.removeAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
private class RequestGroup {
|
typealias Request = URLSessionDataTask
|
||||||
let url: URL
|
|
||||||
private let onFinished: (Data?, UIImage?) -> Void
|
|
||||||
private var task: URLSessionDataTask?
|
|
||||||
private var requests = [Request]()
|
|
||||||
|
|
||||||
init(url: URL, onFinished: @escaping (Data?, UIImage?) -> Void) {
|
|
||||||
self.url = url
|
|
||||||
self.onFinished = onFinished
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
task?.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
func run() {
|
|
||||||
task = URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) in
|
|
||||||
guard error == nil, let data = data else {
|
|
||||||
self.complete(with: nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
self.complete(with: data)
|
|
||||||
})
|
|
||||||
task!.resume()
|
|
||||||
}
|
|
||||||
|
|
||||||
func addCallback(_ completion: ((Data?, UIImage?) -> Void)?) -> Request {
|
|
||||||
let request = Request(callback: completion)
|
|
||||||
requests.append(request)
|
|
||||||
return request
|
|
||||||
}
|
|
||||||
|
|
||||||
func cancelWithoutCallback() {
|
|
||||||
if let request = requests.first(where: { $0.callback == nil && !$0.cancelled }) {
|
|
||||||
request.cancel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fileprivate func requestCancelled() {
|
|
||||||
let remaining = requests.filter { !$0.cancelled }.count
|
|
||||||
if remaining <= 0 {
|
|
||||||
task?.cancel()
|
|
||||||
complete(with: nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func complete(with data: Data?) {
|
|
||||||
let image = data != nil ? UIImage(data: data!) : nil
|
|
||||||
|
|
||||||
requests.filter { !$0.cancelled }.forEach {
|
|
||||||
if let callback = $0.callback {
|
|
||||||
callback(data, image)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.onFinished(data, image)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Request {
|
|
||||||
private weak var group: RequestGroup?
|
|
||||||
private(set) var callback: ((Data?, UIImage?) -> Void)?
|
|
||||||
private(set) var cancelled: Bool = false
|
|
||||||
|
|
||||||
init(callback: ((Data?, UIImage?) -> Void)?) {
|
|
||||||
self.callback = callback
|
|
||||||
}
|
|
||||||
|
|
||||||
func cancel() {
|
|
||||||
cancelled = true
|
|
||||||
callback = nil
|
|
||||||
group?.requestCancelled()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -175,14 +175,4 @@ extension BookmarksTableViewController: UITableViewDataSourcePrefetching, Status
|
||||||
let ids = indexPaths.map { statuses[$0.row].id }
|
let ids = indexPaths.map { statuses[$0.row].id }
|
||||||
prefetchStatuses(with: ids)
|
prefetchStatuses(with: ids)
|
||||||
}
|
}
|
||||||
|
|
||||||
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
|
||||||
let ids: [String] = indexPaths.compactMap {
|
|
||||||
guard $0.row < statuses.count else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return statuses[$0.row].id
|
|
||||||
}
|
|
||||||
cancelPrefetchingStatuses(with: ids)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -467,11 +467,6 @@ extension ConversationTableViewController: UITableViewDataSourcePrefetching, Sta
|
||||||
let ids: [String] = indexPaths.compactMap { item(for: $0)?.id }
|
let ids: [String] = indexPaths.compactMap { item(for: $0)?.id }
|
||||||
prefetchStatuses(with: ids)
|
prefetchStatuses(with: ids)
|
||||||
}
|
}
|
||||||
|
|
||||||
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
|
||||||
let ids: [String] = indexPaths.compactMap { item(for: $0)?.id }
|
|
||||||
cancelPrefetchingStatuses(with: ids)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ConversationTableViewController: ToastableViewController {
|
extension ConversationTableViewController: ToastableViewController {
|
||||||
|
|
|
@ -294,14 +294,4 @@ extension NotificationsTableViewController: UITableViewDataSourcePrefetching {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
|
||||||
for indexPath in indexPaths {
|
|
||||||
guard let group = dataSource.itemIdentifier(for: indexPath)?.group else { continue }
|
|
||||||
for notification in group.notifications {
|
|
||||||
guard let avatar = notification.account.avatar else { continue }
|
|
||||||
ImageCache.avatars.cancelWithoutCallback(avatar)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,22 +30,6 @@ extension StatusTablePrefetching {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func cancelPrefetchingStatuses(with ids: [String]) {
|
|
||||||
let context = apiController.persistentContainer.prefetchBackgroundContext
|
|
||||||
context.perform {
|
|
||||||
guard let statuses = getStatusesWith(ids: ids, in: context) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for status in statuses {
|
|
||||||
guard let avatar = status.account.avatar else { continue }
|
|
||||||
ImageCache.avatars.cancelWithoutCallback(avatar)
|
|
||||||
for attachment in status.attachments where attachment.kind == .image {
|
|
||||||
ImageCache.attachments.cancelWithoutCallback(attachment.url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate func getStatusesWith(ids: [String], in context: NSManagedObjectContext) -> [StatusMO]? {
|
fileprivate func getStatusesWith(ids: [String], in context: NSManagedObjectContext) -> [StatusMO]? {
|
||||||
|
|
Loading…
Reference in New Issue