From d6ae51c02f376d0b5ecb0638fada9634647fae99 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sat, 25 Jan 2020 10:06:27 -0500 Subject: [PATCH] Improve ImageCache loading Keep track of the number of requests and only cancel the underlying URLSessionTask if there are no concrete requsts remaining. Closes #81 --- Tusker/Caching/ImageCache.swift | 94 ++++++++++++++----- .../Attachment/AttachmentViewController.swift | 18 +++- .../BookmarksTableViewController.swift | 8 +- .../ConversationTableViewController.swift | 8 +- .../NotificationsTableViewController.swift | 4 +- .../MyProfileTableViewController.swift | 4 +- .../Profile/ProfileTableViewController.swift | 8 +- .../TimelineTableViewController.swift | 8 +- .../Account Cell/AccountTableViewCell.swift | 14 ++- .../LargeAccountDetailView.swift | 11 ++- Tusker/Views/Attachments/AttachmentView.swift | 13 ++- .../ComposeStatusReplyView.swift | 12 ++- Tusker/Views/ContentTextView.swift | 2 +- .../Instance Cell/InstanceTableViewCell.swift | 14 ++- ...ActionNotificationGroupTableViewCell.swift | 12 +-- ...FollowNotificationGroupTableViewCell.swift | 7 +- ...llowRequestNotificationTableViewCell.swift | 7 +- .../ProfileHeaderTableViewCell.swift | 26 +++-- .../Status/BaseStatusTableViewCell.swift | 15 +-- .../Status/TimelineStatusTableViewCell.swift | 2 +- 20 files changed, 188 insertions(+), 99 deletions(-) diff --git a/Tusker/Caching/ImageCache.swift b/Tusker/Caching/ImageCache.swift index c6b8a586..2e04fbd9 100644 --- a/Tusker/Caching/ImageCache.swift +++ b/Tusker/Caching/ImageCache.swift @@ -12,13 +12,13 @@ import Cache class ImageCache { static let avatars = ImageCache(name: "Avatars", memoryExpiry: .seconds(60 * 60), diskExpiry: .seconds(60 * 60 * 24)) - static let headers = ImageCache(name: "Headers", memoryExpiry: .seconds(60 * 60), diskExpiry: .seconds(60 * 60 * 24)) + static let headers = ImageCache(name: "Headers", memoryExpiry: .seconds(60 * 5), diskExpiry: .seconds(60 * 60)) static let attachments = ImageCache(name: "Attachments", memoryExpiry: .seconds(60 * 2)) static let emojis = ImageCache(name: "Emojis", memoryExpiry: .seconds(60 * 5), diskExpiry: .seconds(60 * 60)) let cache: Cache - var requests = [URL: Request]() + var requests = [URL: RequestGroup]() init(name: String, memoryExpiry expiry: Expiry) { let storage = MemoryStorage(config: MemoryConfig(expiry: expiry)) @@ -36,20 +36,22 @@ class ImageCache { self.cache = .hybrid(HybridStorage(memoryStorage: memory, diskStorage: disk)) } - func get(_ url: URL, completion: ((Data?) -> Void)?) { + func get(_ url: URL, completion: ((Data?) -> Void)?) -> Request? { let key = url.absoluteString if (try? cache.existsObject(forKey: key)) ?? false, let data = try? cache.object(forKey: key) { completion?(data) + return nil } else { - if let completion = completion, let request = requests[url] { - request.callbacks.append(completion) + if let completion = completion, let group = requests[url] { + return group.addCallback(completion) } else { - let request = Request(url: url, completion: completion) - requests[url] = request - request.run { (data) in + let group = RequestGroup(url: url) + let request = group.addCallback(completion) + group.run { (data) in try? self.cache.setObject(data, forKey: key) } + return request } } } @@ -57,24 +59,23 @@ class ImageCache { func get(_ url: URL) -> Data? { return try? cache.object(forKey: url.absoluteString) } - - func cancel(_ url: URL) { - requests[url]?.cancel() + + func cancelWithoutCallback(_ url: URL) { + requests[url]?.cancelWithoutCallback() } - class Request { + class RequestGroup { let url: URL - var task: URLSessionDataTask? - var callbacks: [(Data?) -> Void] - - init(url: URL, completion: ((Data?) -> Void)?) { - if let completion = completion { - self.callbacks = [completion] - } else { - self.callbacks = [] - } + private var task: URLSessionDataTask? + private var requests = [Request]() + + init(url: URL) { self.url = url } + + deinit { + task?.cancel() + } func run(cache: @escaping (Data) -> Void) { task = URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) in @@ -88,13 +89,56 @@ class ImageCache { task!.resume() } - func cancel() { - task?.cancel() - complete(with: nil) + private func updatePriority() { + task?.priority = max(1.0, URLSessionTask.defaultPriority + 0.1 * Float(requests.filter { !$0.cancelled }.count)) } + func addCallback(_ completion: ((Data?) -> Void)?) -> Request { + let request = Request(callback: completion) + requests.append(request) + updatePriority() + return request + } + + func cancelWithoutCallback() { + if let request = requests.first(where: { $0.callback == nil && !$0.cancelled }) { + request.cancel() + updatePriority() + } + } + + fileprivate func requestCancelled() { + let remaining = requests.filter { !$0.cancelled }.count + if remaining <= 0 { + task?.cancel() + complete(with: nil) + } else { + updatePriority() + } + } + func complete(with data: Data?) { - callbacks.forEach { $0(data) } + requests.filter { !$0.cancelled }.forEach { + if let callback = $0.callback { + callback(data) + } + } + } + } + + class Request { + weak var group: RequestGroup? + private(set) var callback: ((Data?) -> Void)? + private(set) var cancelled: Bool = false + + init(callback: ((Data?) -> Void)?) { + self.callback = callback + } + + func cancel() { + cancelled = true + callback = nil + group?.requestCancelled() } } diff --git a/Tusker/Screens/Attachment/AttachmentViewController.swift b/Tusker/Screens/Attachment/AttachmentViewController.swift index 9fb79062..f23fa6d7 100644 --- a/Tusker/Screens/Attachment/AttachmentViewController.swift +++ b/Tusker/Screens/Attachment/AttachmentViewController.swift @@ -15,6 +15,8 @@ class AttachmentViewController: UIViewController { var largeImageVC: LargeImageViewController? var loadingVC: LoadingViewController? + var attachmentRequest: ImageCache.Request? + private var initialControlsVisible: Bool = true var controlsVisible: Bool { get { @@ -51,15 +53,25 @@ class AttachmentViewController: UIViewController { } else { loadingVC = LoadingViewController() embedChild(loadingVC!) - ImageCache.attachments.get(attachment.url) { [weak self] (data) in + attachmentRequest = ImageCache.attachments.get(attachment.url) { [weak self] (data) in + guard let self = self else { return } + self.attachmentRequest = nil DispatchQueue.main.async { - self?.loadingVC?.removeViewAndController() - self?.createLargeImage(data: data!) + self.loadingVC?.removeViewAndController() + self.createLargeImage(data: data!) } } } } + override func didMove(toParent parent: UIViewController?) { + super.didMove(toParent: parent) + + if parent == nil { + attachmentRequest?.cancel() + } + } + func createLargeImage(data: Data) { guard let image = UIImage(data: data) else { return } largeImageVC = LargeImageViewController(image: image, description: attachment.description, sourceInfo: nil) diff --git a/Tusker/Screens/Bookmarks/BookmarksTableViewController.swift b/Tusker/Screens/Bookmarks/BookmarksTableViewController.swift index ce83ee9a..a823ddec 100644 --- a/Tusker/Screens/Bookmarks/BookmarksTableViewController.swift +++ b/Tusker/Screens/Bookmarks/BookmarksTableViewController.swift @@ -159,9 +159,9 @@ extension BookmarksTableViewController: UITableViewDataSourcePrefetching { func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { for indexPath in indexPaths { guard let status = mastodonController.cache.status(for: statuses[indexPath.row].id) else { continue } - ImageCache.avatars.get(status.account.avatar, completion: nil) + _ = ImageCache.avatars.get(status.account.avatar, completion: nil) for attachment in status.attachments where attachment.kind == .image { - ImageCache.attachments.get(attachment.url, completion: nil) + _ = ImageCache.attachments.get(attachment.url, completion: nil) } } } @@ -169,9 +169,9 @@ extension BookmarksTableViewController: UITableViewDataSourcePrefetching { func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) { for indexPath in indexPaths { guard let status = mastodonController.cache.status(for: statuses[indexPath.row].id) else { continue } - ImageCache.avatars.cancel(status.account.avatar) + ImageCache.avatars.cancelWithoutCallback(status.account.avatar) for attachment in status.attachments where attachment.kind == .image { - ImageCache.attachments.cancel(attachment.url) + ImageCache.attachments.cancelWithoutCallback(attachment.url) } } } diff --git a/Tusker/Screens/Conversation/ConversationTableViewController.swift b/Tusker/Screens/Conversation/ConversationTableViewController.swift index b225840d..fb5c99cc 100644 --- a/Tusker/Screens/Conversation/ConversationTableViewController.swift +++ b/Tusker/Screens/Conversation/ConversationTableViewController.swift @@ -170,9 +170,9 @@ extension ConversationTableViewController: UITableViewDataSourcePrefetching { func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { for indexPath in indexPaths { guard let status = mastodonController.cache.status(for: statuses[indexPath.row].id) else { continue } - ImageCache.avatars.get(status.account.avatar, completion: nil) + _ = ImageCache.avatars.get(status.account.avatar, completion: nil) for attachment in status.attachments { - ImageCache.attachments.get(attachment.url, completion: nil) + _ = ImageCache.attachments.get(attachment.url, completion: nil) } } } @@ -180,9 +180,9 @@ extension ConversationTableViewController: UITableViewDataSourcePrefetching { func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) { for indexPath in indexPaths { guard let status = mastodonController.cache.status(for: statuses[indexPath.row].id) else { continue } - ImageCache.avatars.cancel(status.account.avatar) + ImageCache.avatars.cancelWithoutCallback(status.account.avatar) for attachment in status.attachments { - ImageCache.attachments.cancel(attachment.url) + ImageCache.attachments.cancelWithoutCallback(attachment.url) } } } diff --git a/Tusker/Screens/Notifications/NotificationsTableViewController.swift b/Tusker/Screens/Notifications/NotificationsTableViewController.swift index 1ff65686..c76a1456 100644 --- a/Tusker/Screens/Notifications/NotificationsTableViewController.swift +++ b/Tusker/Screens/Notifications/NotificationsTableViewController.swift @@ -240,7 +240,7 @@ extension NotificationsTableViewController: UITableViewDataSourcePrefetching { for indexPath in indexPaths { for notificationID in groups[indexPath.row].notificationIDs { guard let notification = mastodonController.cache.notification(for: notificationID) else { continue } - ImageCache.avatars.get(notification.account.avatar, completion: nil) + _ = ImageCache.avatars.get(notification.account.avatar, completion: nil) } } } @@ -249,7 +249,7 @@ extension NotificationsTableViewController: UITableViewDataSourcePrefetching { for indexPath in indexPaths { for notificationID in groups[indexPath.row].notificationIDs { guard let notification = mastodonController.cache.notification(for: notificationID) else { continue } - ImageCache.avatars.cancel(notification.account.avatar) + ImageCache.avatars.cancelWithoutCallback(notification.account.avatar) } } } diff --git a/Tusker/Screens/Profile/MyProfileTableViewController.swift b/Tusker/Screens/Profile/MyProfileTableViewController.swift index 09ac7884..8e8cdf44 100644 --- a/Tusker/Screens/Profile/MyProfileTableViewController.swift +++ b/Tusker/Screens/Profile/MyProfileTableViewController.swift @@ -21,8 +21,8 @@ class MyProfileTableViewController: ProfileTableViewController { mastodonController.getOwnAccount { (account) in self.accountID = account.id - ImageCache.avatars.get(account.avatar, completion: { (data) in - guard let data = data, let image = UIImage(data: data) else { return } + _ = ImageCache.avatars.get(account.avatar, completion: { [weak self] (data) in + guard let self = self, let data = data, let image = UIImage(data: data) else { return } DispatchQueue.main.async { let size = CGSize(width: 30, height: 30) let tabBarImage = UIGraphicsImageRenderer(size: size).image { (_) in diff --git a/Tusker/Screens/Profile/ProfileTableViewController.swift b/Tusker/Screens/Profile/ProfileTableViewController.swift index ee7a9cac..88b562a3 100644 --- a/Tusker/Screens/Profile/ProfileTableViewController.swift +++ b/Tusker/Screens/Profile/ProfileTableViewController.swift @@ -294,9 +294,9 @@ extension ProfileTableViewController: UITableViewDataSourcePrefetching { for indexPath in indexPaths where indexPath.section > 1 { let statusID = timelineSegments[indexPath.section - 2][indexPath.row].id guard let status = mastodonController.cache.status(for: statusID) else { continue } - ImageCache.avatars.get(status.account.avatar, completion: nil) + _ = ImageCache.avatars.get(status.account.avatar, completion: nil) for attachment in status.attachments { - ImageCache.attachments.get(attachment.url, completion: nil) + _ = ImageCache.attachments.get(attachment.url, completion: nil) } } } @@ -305,9 +305,9 @@ extension ProfileTableViewController: UITableViewDataSourcePrefetching { for indexPath in indexPaths where indexPath.section > 1 { let statusID = timelineSegments[indexPath.section - 2][indexPath.row].id guard let status = mastodonController.cache.status(for: statusID) else { continue } - ImageCache.avatars.cancel(status.account.avatar) + ImageCache.avatars.cancelWithoutCallback(status.account.avatar) for attachment in status.attachments { - ImageCache.attachments.cancel(attachment.url) + ImageCache.attachments.cancelWithoutCallback(attachment.url) } } } diff --git a/Tusker/Screens/Timeline/TimelineTableViewController.swift b/Tusker/Screens/Timeline/TimelineTableViewController.swift index 9966b510..26bda5aa 100644 --- a/Tusker/Screens/Timeline/TimelineTableViewController.swift +++ b/Tusker/Screens/Timeline/TimelineTableViewController.swift @@ -165,9 +165,9 @@ extension TimelineTableViewController: UITableViewDataSourcePrefetching { func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { for indexPath in indexPaths { guard let status = mastodonController.cache.status(for: statusID(for: indexPath)) else { continue } - ImageCache.avatars.get(status.account.avatar, completion: nil) + _ = ImageCache.avatars.get(status.account.avatar, completion: nil) for attachment in status.attachments { - ImageCache.attachments.get(attachment.url, completion: nil) + _ = ImageCache.attachments.get(attachment.url, completion: nil) } } } @@ -175,9 +175,9 @@ extension TimelineTableViewController: UITableViewDataSourcePrefetching { func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) { for indexPath in indexPaths { guard let status = mastodonController.cache.status(for: statusID(for: indexPath)) else { continue } - ImageCache.avatars.cancel(status.account.avatar) + ImageCache.avatars.cancelWithoutCallback(status.account.avatar) for attachment in status.attachments { - ImageCache.attachments.cancel(attachment.url) + ImageCache.attachments.cancelWithoutCallback(attachment.url) } } } diff --git a/Tusker/Views/Account Cell/AccountTableViewCell.swift b/Tusker/Views/Account Cell/AccountTableViewCell.swift index cf01f6ef..e22e1c35 100644 --- a/Tusker/Views/Account Cell/AccountTableViewCell.swift +++ b/Tusker/Views/Account Cell/AccountTableViewCell.swift @@ -19,7 +19,7 @@ class AccountTableViewCell: UITableViewCell { var accountID: String! - var avatarURL: URL? + var avatarRequest: ImageCache.Request? override func awakeFromNib() { super.awakeFromNib() @@ -44,9 +44,9 @@ class AccountTableViewCell: UITableViewCell { fatalError("Missing cached account \(accountID)") } - self.avatarURL = account.avatar - ImageCache.avatars.get(account.avatar) { (data) in - guard let data = data, self.avatarURL == account.avatar else { return } + avatarRequest = ImageCache.avatars.get(account.avatar) { [weak self] (data) in + guard let self = self, let data = data, self.accountID == accountID else { return } + self.avatarRequest = nil DispatchQueue.main.async { self.avatarImageView.image = UIImage(data: data) } @@ -56,6 +56,12 @@ class AccountTableViewCell: UITableViewCell { updateUIForPrefrences() } + + override func prepareForReuse() { + super.prepareForReuse() + + avatarRequest?.cancel() + } } diff --git a/Tusker/Views/Account Detail/LargeAccountDetailView.swift b/Tusker/Views/Account Detail/LargeAccountDetailView.swift index 85084edc..f90389b3 100644 --- a/Tusker/Views/Account Detail/LargeAccountDetailView.swift +++ b/Tusker/Views/Account Detail/LargeAccountDetailView.swift @@ -15,6 +15,8 @@ class LargeAccountDetailView: UIView { var displayNameLabel = UILabel() var usernameLabel = UILabel() + var avatarRequest: ImageCache.Request? + required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) @@ -49,6 +51,10 @@ class LargeAccountDetailView: UIView { NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil) } + deinit { + avatarRequest?.cancel() + } + override func layoutSubviews() { super.layoutSubviews() @@ -63,8 +69,9 @@ class LargeAccountDetailView: UIView { displayNameLabel.text = account.realDisplayName usernameLabel.text = "@\(account.acct)" - ImageCache.avatars.get(account.avatar) { (data) in - guard let data = data else { return } + avatarRequest = ImageCache.avatars.get(account.avatar) { [weak self] (data) in + guard let self = self, let data = data else { return } + self.avatarRequest = nil DispatchQueue.main.async { self.avatarImageView.image = UIImage(data: data) } diff --git a/Tusker/Views/Attachments/AttachmentView.swift b/Tusker/Views/Attachments/AttachmentView.swift index 41bfad86..e64ad498 100644 --- a/Tusker/Views/Attachments/AttachmentView.swift +++ b/Tusker/Views/Attachments/AttachmentView.swift @@ -24,6 +24,8 @@ class AttachmentView: UIImageView, GIFAnimatable { var attachment: Attachment! var index: Int! + private var attachmentRequest: ImageCache.Request? + var gifData: Data? public lazy var animator: Animator? = Animator(withDelegate: self) @@ -37,6 +39,10 @@ class AttachmentView: UIImageView, GIFAnimatable { loadAttachment() } + deinit { + attachmentRequest?.cancel() + } + required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) commonInit() @@ -71,8 +77,9 @@ class AttachmentView: UIImageView, GIFAnimatable { } func loadImage() { - ImageCache.attachments.get(attachment.url) { [weak self] (data) in + attachmentRequest = ImageCache.attachments.get(attachment.url) { [weak self] (data) in guard let self = self, let data = data else { return } + self.attachmentRequest = nil DispatchQueue.main.async { if self.attachment.url.pathExtension == "gif" { self.animate(withGIFData: data) @@ -87,12 +94,14 @@ class AttachmentView: UIImageView, GIFAnimatable { } func loadVideo() { + let attachmentURL = self.attachment.url DispatchQueue.global(qos: .userInitiated).async { - let asset = AVURLAsset(url: self.attachment.url) + let asset = AVURLAsset(url: attachmentURL) let generator = AVAssetImageGenerator(asset: asset) generator.appliesPreferredTrackTransform = true guard let image = try? generator.copyCGImage(at: CMTime(seconds: 0, preferredTimescale: 1), actualTime: nil) else { return } DispatchQueue.main.async { + guard self.attachment.url == attachmentURL else { return } self.image = UIImage(cgImage: image) } } diff --git a/Tusker/Views/Compose Status Reply/ComposeStatusReplyView.swift b/Tusker/Views/Compose Status Reply/ComposeStatusReplyView.swift index 12f24cd5..f4f6f73f 100644 --- a/Tusker/Views/Compose Status Reply/ComposeStatusReplyView.swift +++ b/Tusker/Views/Compose Status Reply/ComposeStatusReplyView.swift @@ -18,17 +18,23 @@ class ComposeStatusReplyView: UIView { @IBOutlet weak var usernameLabel: UILabel! @IBOutlet weak var statusContentTextView: StatusContentTextView! + var avatarRequest: ImageCache.Request? + static func create() -> ComposeStatusReplyView { return UINib(nibName: "ComposeStatusReplyView", bundle: nil).instantiate(withOwner: nil, options: nil).first as! ComposeStatusReplyView } + deinit { + avatarRequest?.cancel() + } + override func awakeFromNib() { super.awakeFromNib() updateUIForPreferences() NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil) } - + @objc func updateUIForPreferences() { avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView) } @@ -39,8 +45,8 @@ class ComposeStatusReplyView: UIView { statusContentTextView.overrideMastodonController = mastodonController statusContentTextView.statusID = status.id - ImageCache.avatars.get(status.account.avatar) { (data) in - guard let data = data else { return } + avatarRequest = ImageCache.avatars.get(status.account.avatar) { [weak self] (data) in + guard let self = self, let data = data else { return } DispatchQueue.main.async { self.avatarImageView.image = UIImage(data: data) } diff --git a/Tusker/Views/ContentTextView.swift b/Tusker/Views/ContentTextView.swift index ccf245ea..36309199 100644 --- a/Tusker/Views/ContentTextView.swift +++ b/Tusker/Views/ContentTextView.swift @@ -49,7 +49,7 @@ class ContentTextView: LinkTextView { for emoji in emojis { group.enter() - ImageCache.emojis.get(emoji.url) { (data) in + _ = ImageCache.emojis.get(emoji.url) { (data) in defer { group.leave() } guard let data = data, let image = UIImage(data: data) else { return diff --git a/Tusker/Views/Instance Cell/InstanceTableViewCell.swift b/Tusker/Views/Instance Cell/InstanceTableViewCell.swift index 161015b3..55d890ae 100644 --- a/Tusker/Views/Instance Cell/InstanceTableViewCell.swift +++ b/Tusker/Views/Instance Cell/InstanceTableViewCell.swift @@ -20,6 +20,7 @@ class InstanceTableViewCell: UITableViewCell { var selectorInstance: InstanceSelector.Instance? var thumbnailURL: URL? + var thumbnailRequest: ImageCache.Request? override func awakeFromNib() { super.awakeFromNib() @@ -59,12 +60,21 @@ class InstanceTableViewCell: UITableViewCell { private func updateThumbnail(url: URL) { thumbnailImageView.image = nil thumbnailURL = url - ImageCache.attachments.get(url) { (data) in - guard self.thumbnailURL == url, let data = data, let image = UIImage(data: data) else { return } + thumbnailRequest = ImageCache.attachments.get(url) { [weak self] (data) in + guard let self = self, self.thumbnailURL == url, let data = data, let image = UIImage(data: data) else { return } + self.thumbnailRequest = nil DispatchQueue.main.async { self.thumbnailImageView.image = image } } } + + override func prepareForReuse() { + super.prepareForReuse() + + thumbnailRequest?.cancel() + instance = nil + selectorInstance = nil + } } diff --git a/Tusker/Views/Notifications/ActionNotificationGroupTableViewCell.swift b/Tusker/Views/Notifications/ActionNotificationGroupTableViewCell.swift index d4ea85cb..597c3739 100644 --- a/Tusker/Views/Notifications/ActionNotificationGroupTableViewCell.swift +++ b/Tusker/Views/Notifications/ActionNotificationGroupTableViewCell.swift @@ -24,7 +24,7 @@ class ActionNotificationGroupTableViewCell: UITableViewCell { var group: NotificationGroup! var statusID: String! - var authorAvatarURL: URL? + var avatarRequests = [String: ImageCache.Request]() var updateTimestampWorkItem: DispatchWorkItem? deinit { @@ -75,9 +75,10 @@ class ActionNotificationGroupTableViewCell: UITableViewCell { imageView.translatesAutoresizingMaskIntoConstraints = false imageView.layer.masksToBounds = true imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * 30 - ImageCache.avatars.get(account.avatar) { (data) in - guard let data = data, self.group.id == group.id else { return } + avatarRequests[account.id] = ImageCache.avatars.get(account.avatar) { [weak self] (data) in + guard let self = self, let data = data, self.group.id == group.id else { return } DispatchQueue.main.async { + self.avatarRequests.removeValue(forKey: account.id) imageView.image = UIImage(data: data) } } @@ -149,10 +150,7 @@ class ActionNotificationGroupTableViewCell: UITableViewCell { override func prepareForReuse() { super.prepareForReuse() - if let authorAvatarURL = authorAvatarURL { - ImageCache.avatars.cancel(authorAvatarURL) - } - + avatarRequests.values.forEach { $0.cancel() } updateTimestampWorkItem?.cancel() updateTimestampWorkItem = nil } diff --git a/Tusker/Views/Notifications/FollowNotificationGroupTableViewCell.swift b/Tusker/Views/Notifications/FollowNotificationGroupTableViewCell.swift index 90d982fd..26c825c1 100644 --- a/Tusker/Views/Notifications/FollowNotificationGroupTableViewCell.swift +++ b/Tusker/Views/Notifications/FollowNotificationGroupTableViewCell.swift @@ -20,6 +20,7 @@ class FollowNotificationGroupTableViewCell: UITableViewCell { var group: NotificationGroup! + var avatarRequests = [String: ImageCache.Request]() var updateTimestampWorkItem: DispatchWorkItem? deinit { @@ -55,9 +56,10 @@ class FollowNotificationGroupTableViewCell: UITableViewCell { imageView.translatesAutoresizingMaskIntoConstraints = false imageView.layer.masksToBounds = true imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * 30 - ImageCache.avatars.get(account.avatar) { (data) in - guard let data = data, self.group.id == group.id else { return } + avatarRequests[account.id] = ImageCache.avatars.get(account.avatar) { [weak self] (data) in + guard let self = self, let data = data, self.group.id == group.id else { return } DispatchQueue.main.async { + self.avatarRequests.removeValue(forKey: account.id) imageView.image = UIImage(data: data) } } @@ -114,6 +116,7 @@ class FollowNotificationGroupTableViewCell: UITableViewCell { override func prepareForReuse() { super.prepareForReuse() + avatarRequests.values.forEach { $0.cancel() } updateTimestampWorkItem?.cancel() updateTimestampWorkItem = nil } diff --git a/Tusker/Views/Notifications/FollowRequestNotificationTableViewCell.swift b/Tusker/Views/Notifications/FollowRequestNotificationTableViewCell.swift index f31fe7a1..a483b3ac 100644 --- a/Tusker/Views/Notifications/FollowRequestNotificationTableViewCell.swift +++ b/Tusker/Views/Notifications/FollowRequestNotificationTableViewCell.swift @@ -25,6 +25,7 @@ class FollowRequestNotificationTableViewCell: UITableViewCell { var notification: Pachyderm.Notification? var account: Account! + var avatarRequest: ImageCache.Request? var updateTimestampWorkItem: DispatchWorkItem? deinit { @@ -53,8 +54,9 @@ class FollowRequestNotificationTableViewCell: UITableViewCell { func updateUI(account: Account) { self.account = account actionLabel.text = "Request to follow from \(account.realDisplayName)" - ImageCache.avatars.get(account.avatar) { (data) in - guard self.account == account, let data = data, let image = UIImage(data: data) else { return } + avatarRequest = ImageCache.avatars.get(account.avatar) { [weak self] (data) in + guard let self = self, self.account == account, let data = data, let image = UIImage(data: data) else { return } + self.avatarRequest = nil DispatchQueue.main.async { self.avatarImageView.image = image } @@ -88,6 +90,7 @@ class FollowRequestNotificationTableViewCell: UITableViewCell { override func prepareForReuse() { super.prepareForReuse() + avatarRequest?.cancel() updateTimestampWorkItem?.cancel() updateTimestampWorkItem = nil } diff --git a/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.swift b/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.swift index bf740fa5..c747d94b 100644 --- a/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.swift +++ b/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.swift @@ -32,8 +32,8 @@ class ProfileHeaderTableViewCell: UITableViewCell { var accountID: String! - var avatarURL: URL? - var headerURL: URL? + var avatarRequest: ImageCache.Request? + var headerRequest: ImageCache.Request? override func awakeFromNib() { avatarContainerView.layer.masksToBounds = true @@ -63,19 +63,19 @@ class ProfileHeaderTableViewCell: UITableViewCell { usernameLabel.text = "@\(account.acct)" avatarImageView.image = nil - avatarURL = account.avatar - ImageCache.avatars.get(account.avatar) { (data) in - guard let data = data else { return } + avatarRequest = ImageCache.avatars.get(account.avatar) { [weak self] (data) in + guard let self = self, let data = data, self.accountID == accountID else { return } + self.avatarRequest = nil DispatchQueue.main.async { self.avatarImageView.image = UIImage(data: data) - self.avatarURL = nil } } - ImageCache.headers.get(account.header) { (data) in - guard let data = data else { return } + headerImageView.image = nil + headerRequest = ImageCache.headers.get(account.header) { [weak self] (data) in + guard let self = self, let data = data, self.accountID == accountID else { return } + self.headerRequest = nil DispatchQueue.main.async { self.headerImageView.image = UIImage(data: data) - self.headerURL = nil } } @@ -131,12 +131,8 @@ class ProfileHeaderTableViewCell: UITableViewCell { } override func prepareForReuse() { - if let url = avatarURL { - ImageCache.avatars.cancel(url) - } - if let url = headerURL { - ImageCache.headers.cancel(url) - } + avatarRequest?.cancel() + headerRequest?.cancel() } @objc func morePressed() { diff --git a/Tusker/Views/Status/BaseStatusTableViewCell.swift b/Tusker/Views/Status/BaseStatusTableViewCell.swift index 6d43c28e..bf0fe2a8 100644 --- a/Tusker/Views/Status/BaseStatusTableViewCell.swift +++ b/Tusker/Views/Status/BaseStatusTableViewCell.swift @@ -64,9 +64,8 @@ class BaseStatusTableViewCell: UITableViewCell { } var showStatusAutomatically = false - var avatarURL: URL? - var attachmentDataTasks: [URLSessionDataTask] = [] - + private var avatarRequest: ImageCache.Request? + private var statusUpdater: Cancellable? private var accountUpdater: Cancellable? @@ -180,12 +179,10 @@ class BaseStatusTableViewCell: UITableViewCell { func updateUI(account: Account) { usernameLabel.text = "@\(account.acct)" avatarImageView.image = nil - avatarURL = account.avatar - ImageCache.avatars.get(account.avatar) { (data) in - guard let data = data, self.avatarURL == account.avatar else { return } + avatarRequest = ImageCache.avatars.get(account.avatar) { [weak self] (data) in + guard let self = self, let data = data, self.accountID == account.id else { return } DispatchQueue.main.async { self.avatarImageView.image = UIImage(data: data) - self.avatarURL = nil } } } @@ -200,9 +197,7 @@ class BaseStatusTableViewCell: UITableViewCell { override func prepareForReuse() { super.prepareForReuse() - if let avatarURL = avatarURL { - ImageCache.avatars.cancel(avatarURL) - } + avatarRequest?.cancel() attachmentsView.attachmentViews.allObjects.forEach { $0.removeFromSuperview() } showStatusAutomatically = false } diff --git a/Tusker/Views/Status/TimelineStatusTableViewCell.swift b/Tusker/Views/Status/TimelineStatusTableViewCell.swift index 44a9680b..4c1181da 100644 --- a/Tusker/Views/Status/TimelineStatusTableViewCell.swift +++ b/Tusker/Views/Status/TimelineStatusTableViewCell.swift @@ -95,7 +95,7 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell { } func updateTimestamp() { - guard let status = mastodonController.cache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") } + guard let mastodonController = mastodonController, let status = mastodonController.cache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") } timestampLabel.text = status.createdAt.timeAgoString() timestampLabel.accessibilityLabel = TimelineStatusTableViewCell.relativeDateFormatter.localizedString(for: status.createdAt, relativeTo: Date())