From c12d2db258e4d3fc65cc5d114d1fcf822b89dced Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sat, 16 Jan 2021 15:24:15 -0500 Subject: [PATCH] Cache UIImage objects to avoid re-decoding images unnecessarily --- Tusker.xcodeproj/project.pbxproj | 4 + .../AccountActivityItemSource.swift | 2 +- .../Activities/StatusActivityItemSource.swift | 2 +- Tusker/Caching/ImageCache.swift | 56 +++++++------- Tusker/Caching/ImageDataCache.swift | 74 +++++++++++++++++++ Tusker/ImageGrayscalifier.swift | 9 +++ .../AttachmentPreviewViewController.swift | 2 +- .../GalleryViewController.swift | 2 +- .../Compose/ComposeAvatarImageView.swift | 14 +--- .../Compose/EmojiCollectionViewCell.swift | 11 ++- .../FastSwitchingAccountView.swift | 4 +- .../LoadingLargeImageViewController.swift | 21 ++---- .../Preferences/LocalAccountAvatarView.swift | 8 +- .../Profile/MyProfileViewController.swift | 17 ++--- .../Account Cell/AccountTableViewCell.swift | 15 ++-- .../LargeAccountDetailView.swift | 6 +- Tusker/Views/AccountDisplayNameLabel.swift | 4 +- Tusker/Views/Attachments/AttachmentView.swift | 2 +- Tusker/Views/BaseEmojiLabel.swift | 15 +--- Tusker/Views/ContentTextView.swift | 15 +--- Tusker/Views/CustomEmojiImageView.swift | 12 +-- .../Instance Cell/InstanceTableViewCell.swift | 4 +- ...ActionNotificationGroupTableViewCell.swift | 46 ++++++------ ...FollowNotificationGroupTableViewCell.swift | 45 +++++------ ...llowRequestNotificationTableViewCell.swift | 19 ++--- .../Profile Header/ProfileHeaderView.swift | 24 +++--- .../Status/BaseStatusTableViewCell.swift | 16 ++-- Tusker/Views/Status/StatusCardView.swift | 17 ++--- 28 files changed, 243 insertions(+), 223 deletions(-) create mode 100644 Tusker/Caching/ImageDataCache.swift diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index be8ad314..6c476a48 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -112,6 +112,7 @@ D62D2424217ABF3F005076CC /* NSUserActivity+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2423217ABF3F005076CC /* NSUserActivity+Extensions.swift */; }; D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2425217ABF63005076CC /* UserActivityType.swift */; }; D62FF04823D7CDD700909D6E /* AttributedStringHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62FF04723D7CDD700909D6E /* AttributedStringHelperTests.swift */; }; + D6311C5025B3765B00B27539 /* ImageDataCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6311C4F25B3765B00B27539 /* ImageDataCache.swift */; }; D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B362137838300CE884A /* AttributedString+Helpers.swift */; }; D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */; }; D63569E023908A8D003DD353 /* StatusState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60A4FFB238B726A008AC647 /* StatusState.swift */; }; @@ -468,6 +469,7 @@ D62D2423217ABF3F005076CC /* NSUserActivity+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSUserActivity+Extensions.swift"; sourceTree = ""; }; D62D2425217ABF63005076CC /* UserActivityType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivityType.swift; sourceTree = ""; }; D62FF04723D7CDD700909D6E /* AttributedStringHelperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringHelperTests.swift; sourceTree = ""; }; + D6311C4F25B3765B00B27539 /* ImageDataCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDataCache.swift; sourceTree = ""; }; D6333B362137838300CE884A /* AttributedString+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttributedString+Helpers.swift"; sourceTree = ""; }; D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+TimeAgo.swift"; sourceTree = ""; }; D63661BF2381C144004B9E16 /* PreferencesNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesNavigationController.swift; sourceTree = ""; }; @@ -1493,6 +1495,7 @@ children = ( D6F1F84C2193B56E00F5FE67 /* Cache.swift */, 04DACE8D212CC7CC009840C4 /* ImageCache.swift */, + D6311C4F25B3765B00B27539 /* ImageDataCache.swift */, ); path = Caching; sourceTree = ""; @@ -1889,6 +1892,7 @@ D627943723A552C200D38C68 /* BookmarkStatusActivity.swift in Sources */, D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */, D6AC956723C4347E008C9946 /* MainSceneDelegate.swift in Sources */, + D6311C5025B3765B00B27539 /* ImageDataCache.swift in Sources */, D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */, D60E2F2E244248BF005F8713 /* MastodonCachePersistentStore.swift in Sources */, D620483623D38075008A63EF /* ContentTextView.swift in Sources */, diff --git a/Tusker/Activities/AccountActivityItemSource.swift b/Tusker/Activities/AccountActivityItemSource.swift index ed32e529..f1fde366 100644 --- a/Tusker/Activities/AccountActivityItemSource.swift +++ b/Tusker/Activities/AccountActivityItemSource.swift @@ -29,7 +29,7 @@ class AccountActivityItemSource: NSObject, UIActivityItemSource { metadata.originalURL = account.url metadata.url = account.url metadata.title = "\(account.displayName) (@\(account.username)@\(account.url.host!)" - if let data = ImageCache.avatars.get(account.avatar), + if let data = ImageCache.avatars.getData(account.avatar), let image = UIImage(data: data) { metadata.iconProvider = NSItemProvider(object: image) } diff --git a/Tusker/Activities/StatusActivityItemSource.swift b/Tusker/Activities/StatusActivityItemSource.swift index dc0d78c9..3240413c 100644 --- a/Tusker/Activities/StatusActivityItemSource.swift +++ b/Tusker/Activities/StatusActivityItemSource.swift @@ -32,7 +32,7 @@ class StatusActivityItemSource: NSObject, UIActivityItemSource { let doc = try! SwiftSoup.parse(status.content) let content = try! doc.text() metadata.title = "\(status.account.displayName): \"\(content)\"" - if let data = ImageCache.avatars.get(status.account.avatar), + if let data = ImageCache.avatars.getData(status.account.avatar), let image = UIImage(data: data) { metadata.iconProvider = NSItemProvider(object: image) } diff --git a/Tusker/Caching/ImageCache.swift b/Tusker/Caching/ImageCache.swift index 6475abbb..9c09434d 100644 --- a/Tusker/Caching/ImageCache.swift +++ b/Tusker/Caching/ImageCache.swift @@ -22,47 +22,36 @@ class ImageCache { private static let disableCaching = false #endif - private let cache: Cache + private let cache: ImageDataCache private var groups = MultiThreadDictionary(name: "ImageCache request groups") private var backgroundQueue = DispatchQueue(label: "ImageCache completion queue", qos: .default) - init(name: String, memoryExpiry expiry: Expiry) { - let storage = MemoryStorage(config: MemoryConfig(expiry: expiry)) - self.cache = .memory(storage) + init(name: String, memoryExpiry: Expiry, diskExpiry: Expiry? = nil) { + self.cache = ImageDataCache(name: name, memoryExpiry: memoryExpiry, diskExpiry: diskExpiry) } - init(name: String, diskExpiry expiry: Expiry) { - let storage = try! DiskStorage(config: DiskConfig(name: name, expiry: expiry), transformer: TransformerFactory.forData()) - self.cache = .disk(storage) - } - - init(name: String, memoryExpiry: Expiry, diskExpiry: Expiry) { - let memory = MemoryStorage(config: MemoryConfig(expiry: memoryExpiry)) - let disk = try! DiskStorage(config: DiskConfig(name: name, expiry: diskExpiry), transformer: TransformerFactory.forData()) - self.cache = .hybrid(HybridStorage(memoryStorage: memory, diskStorage: disk)) - } - - func get(_ url: URL, completion: ((Data?) -> Void)?) -> Request? { + func get(_ url: URL, completion: ((Data?, UIImage?) -> Void)?) -> Request? { let key = url.absoluteString if !ImageCache.disableCaching, // todo: calling object(forKey: key) does disk I/O and this method is often called from the main thread // in performance sensitive paths. a nice optimization to DiskStorage would be adding an internal cache // of the state (unknown/exists/does not exist) of whether or not objects exist on disk so that the slow, disk I/O // path can be avoided most of the time - let data = try? cache.object(forKey: key) { + let (data, image) = try? cache.get(key) { backgroundQueue.async { - completion?(data) + completion?(data, image) } return nil } else { if let completion = completion, let group = groups[url] { return group.addCallback(completion) } else { - let group = RequestGroup(url: url) { (data) in - if let data = data { - try? self.cache.setObject(data, forKey: key) + let group = RequestGroup(url: url) { (data, image) in + if let data = data, + let image = UIImage(data: data) { + try? self.cache.set(key, data: data, image: image) } self.groups.removeValueWithoutReturning(forKey: url) } @@ -74,8 +63,12 @@ class ImageCache { } } - func get(_ url: URL) -> Data? { - return try? cache.object(forKey: url.absoluteString) + func getData(_ url: URL) -> Data? { + return try? cache.getData(url.absoluteString) + } + + func get(_ url: URL) -> (Data, UIImage)? { + return try? cache.get(url.absoluteString) } func cancelWithoutCallback(_ url: URL) { @@ -88,11 +81,11 @@ class ImageCache { private class RequestGroup { let url: URL - private let onFinished: (Data?) -> Void + private let onFinished: (Data?, UIImage?) -> Void private var task: URLSessionDataTask? private var requests = [Request]() - init(url: URL, onFinished: @escaping (Data?) -> Void) { + init(url: URL, onFinished: @escaping (Data?, UIImage?) -> Void) { self.url = url self.onFinished = onFinished } @@ -116,7 +109,7 @@ class ImageCache { task?.priority = max(1.0, URLSessionTask.defaultPriority + 0.1 * Float(requests.filter { !$0.cancelled }.count)) } - func addCallback(_ completion: ((Data?) -> Void)?) -> Request { + func addCallback(_ completion: ((Data?, UIImage?) -> Void)?) -> Request { let request = Request(callback: completion) requests.append(request) updatePriority() @@ -141,21 +134,24 @@ class ImageCache { } 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) + callback(data, image) } } - self.onFinished(data) + + self.onFinished(data, image) } } class Request { private weak var group: RequestGroup? - private(set) var callback: ((Data?) -> Void)? + private(set) var callback: ((Data?, UIImage?) -> Void)? private(set) var cancelled: Bool = false - init(callback: ((Data?) -> Void)?) { + init(callback: ((Data?, UIImage?) -> Void)?) { self.callback = callback } diff --git a/Tusker/Caching/ImageDataCache.swift b/Tusker/Caching/ImageDataCache.swift new file mode 100644 index 00000000..e548491a --- /dev/null +++ b/Tusker/Caching/ImageDataCache.swift @@ -0,0 +1,74 @@ +// +// ImageDataCache.swift +// Tusker +// +// Created by Shadowfacts on 1/16/21. +// Copyright © 2021 Shadowfacts. All rights reserved. +// + +import UIKit +import Cache + +class ImageDataCache { + + private let memory: MemoryStorage<(Data, UIImage)> + private let disk: DiskStorage? + + init(name: String, memoryExpiry: Expiry, diskExpiry: Expiry?) { + let memoryConfig = MemoryConfig(expiry: memoryExpiry) + self.memory = MemoryStorage(config: memoryConfig) + + if let diskExpiry = diskExpiry { + let diskConfig = DiskConfig(name: name, expiry: diskExpiry) + self.disk = try! DiskStorage(config: diskConfig, transformer: TransformerFactory.forData()) + } else { + self.disk = nil + } + } + + func has(_ key: String) throws -> Bool { + if try memory.existsObject(forKey: key) { + return true + } else if let disk = self.disk, + try disk.existsObject(forKey: key) { + return true + } else { + return false + } + } + + func get(_ key: String) throws -> (Data, UIImage)? { + if try memory.existsObject(forKey: key) { + return try! memory.object(forKey: key) + } else if let disk = self.disk, + try disk.existsObject(forKey: key), + let data = try? disk.object(forKey: key), + let image = UIImage(data: data) { + return (data, image) + } else { + return nil + } + } + + func getImage(_ key: String) throws -> UIImage? { + return try get(key)?.1 + } + + func getData(_ key: String) throws -> Data? { + return try get(key)?.0 + } + + func set(_ key: String, data: Data, image: UIImage) throws { + memory.setObject((data, image), forKey: key) + + if let disk = self.disk { + try disk.setObject(data, forKey: key) + } + } + + func removeAll() throws { + memory.removeAll() + try? disk?.removeAll() + } + +} diff --git a/Tusker/ImageGrayscalifier.swift b/Tusker/ImageGrayscalifier.swift index 872ef7b5..63e01ca3 100644 --- a/Tusker/ImageGrayscalifier.swift +++ b/Tusker/ImageGrayscalifier.swift @@ -14,6 +14,15 @@ struct ImageGrayscalifier { private static let context = CIContext() private static let cache = NSCache() + static func convertIfNecessary(url: URL?, image: UIImage) -> UIImage? { + if Preferences.shared.grayscaleImages, + let source = image.cgImage { + return convert(url: url, cgImage: source) + } else { + return image + } + } + static func convert(url: URL?, data: Data) -> UIImage? { if let url = url, let cached = cache.object(forKey: url as NSURL) { diff --git a/Tusker/Screens/Attachment Gallery/AttachmentPreviewViewController.swift b/Tusker/Screens/Attachment Gallery/AttachmentPreviewViewController.swift index 64aee4c0..2c723efd 100644 --- a/Tusker/Screens/Attachment Gallery/AttachmentPreviewViewController.swift +++ b/Tusker/Screens/Attachment Gallery/AttachmentPreviewViewController.swift @@ -25,7 +25,7 @@ class AttachmentPreviewViewController: UIViewController { } override func loadView() { - if let data = ImageCache.attachments.get(attachment.url), + if let data = ImageCache.attachments.getData(attachment.url), let image = UIImage(data: data) { let imageView: UIImageView if attachment.url.pathExtension == "gif" { diff --git a/Tusker/Screens/Attachment Gallery/GalleryViewController.swift b/Tusker/Screens/Attachment Gallery/GalleryViewController.swift index dbcbf8ca..02135b81 100644 --- a/Tusker/Screens/Attachment Gallery/GalleryViewController.swift +++ b/Tusker/Screens/Attachment Gallery/GalleryViewController.swift @@ -44,7 +44,7 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc var animationGifData: Data? { let attachment = attachments[currentIndex] if attachment.url.pathExtension == "gif" { - return ImageCache.attachments.get(attachment.url) + return ImageCache.attachments.getData(attachment.url) } else { return nil } diff --git a/Tusker/Screens/Compose/ComposeAvatarImageView.swift b/Tusker/Screens/Compose/ComposeAvatarImageView.swift index 8004869d..eceb9c3a 100644 --- a/Tusker/Screens/Compose/ComposeAvatarImageView.swift +++ b/Tusker/Screens/Compose/ComposeAvatarImageView.swift @@ -44,16 +44,10 @@ struct ComposeAvatarImageView: View { private func loadImage() { guard let url = url else { return } - request = ImageCache.avatars.get(url) { (data) in - if let data = data, let image = UIImage(data: data) { - DispatchQueue.main.async { - self.request = nil - self.avatarImage = image - } - } else { - DispatchQueue.main.async { - self.request = nil - } + request = ImageCache.avatars.get(url) { (_, image) in + DispatchQueue.main.async { + self.request = nil + self.avatarImage = image } } } diff --git a/Tusker/Screens/Compose/EmojiCollectionViewCell.swift b/Tusker/Screens/Compose/EmojiCollectionViewCell.swift index 7e4d71e3..d23fc531 100644 --- a/Tusker/Screens/Compose/EmojiCollectionViewCell.swift +++ b/Tusker/Screens/Compose/EmojiCollectionViewCell.swift @@ -45,12 +45,11 @@ class EmojiCollectionViewCell: UICollectionViewCell { func updateUI(emoji: Emoji) { currentEmojiShortcode = emoji.shortcode - imageRequest = ImageCache.emojis.get(emoji.url) { [weak self] (data) in - if let data = data, let image = UIImage(data: data) { - DispatchQueue.main.async { [weak self] in - guard let self = self, self.currentEmojiShortcode == emoji.shortcode else { return } - self.emojiImageView.image = image - } + imageRequest = ImageCache.emojis.get(emoji.url) { [weak self] (_, image) in + guard let image = image else { return } + DispatchQueue.main.async { [weak self] in + guard let self = self, self.currentEmojiShortcode == emoji.shortcode else { return } + self.emojiImageView.image = image } } } diff --git a/Tusker/Screens/Fast Account Switcher/FastSwitchingAccountView.swift b/Tusker/Screens/Fast Account Switcher/FastSwitchingAccountView.swift index 5a047842..f306b5f0 100644 --- a/Tusker/Screens/Fast Account Switcher/FastSwitchingAccountView.swift +++ b/Tusker/Screens/Fast Account Switcher/FastSwitchingAccountView.swift @@ -87,8 +87,8 @@ class FastSwitchingAccountView: UIView { let controller = MastodonController.getForAccount(account) controller.getOwnAccount { [weak self] (result) in guard let self = self, case let .success(account) = result else { return } - self.avatarRequest = ImageCache.avatars.get(account.avatar) { [weak avatarImageView] (data) in - guard let avatarImageView = avatarImageView, let data = data, let image = UIImage(data: data) else { return } + self.avatarRequest = ImageCache.avatars.get(account.avatar) { [weak avatarImageView] (_, image) in + guard let avatarImageView = avatarImageView, let image = image else { return } DispatchQueue.main.async { avatarImageView.image = image } diff --git a/Tusker/Screens/Large Image/LoadingLargeImageViewController.swift b/Tusker/Screens/Large Image/LoadingLargeImageViewController.swift index 18a390f8..24b33afe 100644 --- a/Tusker/Screens/Large Image/LoadingLargeImageViewController.swift +++ b/Tusker/Screens/Large Image/LoadingLargeImageViewController.swift @@ -85,19 +85,19 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie overrideUserInterfaceStyle = .dark view.backgroundColor = .black - if let data = cache.get(url) { - createLargeImage(data: data, url: url) + if let (data, image) = cache.get(url) { + createLargeImage(data: data, image: image, url: url) } else { createPreview() loadingVC = LoadingViewController() embedChild(loadingVC!) - imageRequest = cache.get(url) { [weak self] (data) in + imageRequest = cache.get(url) { [weak self] (data, image) in guard let self = self else { return } self.imageRequest = nil DispatchQueue.main.async { self.loadingVC?.removeViewAndController() - self.createLargeImage(data: data!, url: self.url) + self.createLargeImage(data: data!, image: image!, url: self.url) } } } @@ -115,20 +115,13 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie } } - private func createLargeImage(data: Data, url: URL) { + private func createLargeImage(data: Data, image: UIImage, url: URL) { guard !loaded else { return } loaded = true - let image: UIImage? - if Preferences.shared.grayscaleImages { - image = ImageGrayscalifier.convert(url: url, data: data) - } else { - image = UIImage(data: data) - } - - if let image = image { + if let transformedImage = ImageGrayscalifier.convertIfNecessary(url: url, image: image) { let gifData = url.pathExtension == "gif" ? data : nil - createLargeImage(image: image, gifData: gifData) + createLargeImage(image: transformedImage, gifData: gifData) } } diff --git a/Tusker/Screens/Preferences/LocalAccountAvatarView.swift b/Tusker/Screens/Preferences/LocalAccountAvatarView.swift index d039b2a5..46fa54d4 100644 --- a/Tusker/Screens/Preferences/LocalAccountAvatarView.swift +++ b/Tusker/Screens/Preferences/LocalAccountAvatarView.swift @@ -38,11 +38,9 @@ struct LocalAccountAvatarView: View { let controller = MastodonController.getForAccount(localAccountInfo) controller.getOwnAccount { (result) in guard case let .success(account) = result else { return } - _ = ImageCache.avatars.get(account.avatar) { (data) in - if let data = data, let image = UIImage(data: data) { - DispatchQueue.main.async { - self.avatarImage = image - } + _ = ImageCache.avatars.get(account.avatar) { (_, image) in + DispatchQueue.main.async { + self.avatarImage = image } } } diff --git a/Tusker/Screens/Profile/MyProfileViewController.swift b/Tusker/Screens/Profile/MyProfileViewController.swift index 4219fd1e..f702142d 100644 --- a/Tusker/Screens/Profile/MyProfileViewController.swift +++ b/Tusker/Screens/Profile/MyProfileViewController.swift @@ -43,17 +43,10 @@ class MyProfileViewController: ProfileViewController { private func setAvatarTabBarImage(account: Account) { let avatarURL = account.avatar - _ = ImageCache.avatars.get(avatarURL, completion: { [weak self] (data) in - guard let self = self, let data = data else { return } - - let maybeGrayscale: UIImage? - if Preferences.shared.grayscaleImages { - maybeGrayscale = ImageGrayscalifier.convert(url: avatarURL, data: data) - } else { - maybeGrayscale = UIImage(data: data) - } - - guard let image = maybeGrayscale else { + _ = ImageCache.avatars.get(avatarURL, completion: { [weak self] (_, image) in + guard let self = self, + let image = image, + let maybeGrayscale = ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) else { return } @@ -63,7 +56,7 @@ class MyProfileViewController: ProfileViewController { let tabBarImage = UIGraphicsImageRenderer(size: size).image { (_) in let radius = Preferences.shared.avatarStyle.cornerRadiusFraction * 30 UIBezierPath(roundedRect: rect, cornerRadius: radius).addClip() - image.draw(in: rect) + maybeGrayscale.draw(in: rect) } let alwaysOriginalImage = tabBarImage.withRenderingMode(.alwaysOriginal) self.tabBarItem.image = alwaysOriginalImage diff --git a/Tusker/Views/Account Cell/AccountTableViewCell.swift b/Tusker/Views/Account Cell/AccountTableViewCell.swift index 03e52757..8777ec94 100644 --- a/Tusker/Views/Account Cell/AccountTableViewCell.swift +++ b/Tusker/Views/Account Cell/AccountTableViewCell.swift @@ -63,19 +63,16 @@ class AccountTableViewCell: UITableViewCell { let accountID = self.accountID let avatarURL = account.avatar - avatarRequest = ImageCache.avatars.get(avatarURL) { [weak self] (data) in - guard let self = self, let data = data, self.accountID == accountID else { return } + avatarRequest = ImageCache.avatars.get(avatarURL) { [weak self] (_, image) in + guard let self = self else { return } self.avatarRequest = nil - let image: UIImage? - if self.isGrayscale { - image = ImageGrayscalifier.convert(url: avatarURL, data: data) - } else { - image = UIImage(data: data) - } + guard let image = image, + self.accountID == accountID, + let transformedImage = ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) else { return } DispatchQueue.main.async { - self.avatarImageView.image = image + self.avatarImageView.image = transformedImage } } diff --git a/Tusker/Views/Account Detail/LargeAccountDetailView.swift b/Tusker/Views/Account Detail/LargeAccountDetailView.swift index 0335fd2a..e0a4feb9 100644 --- a/Tusker/Views/Account Detail/LargeAccountDetailView.swift +++ b/Tusker/Views/Account Detail/LargeAccountDetailView.swift @@ -69,11 +69,11 @@ class LargeAccountDetailView: UIView { displayNameLabel.updateForAccountDisplayName(account: account) usernameLabel.text = "@\(account.acct)" - avatarRequest = ImageCache.avatars.get(account.avatar) { [weak self] (data) in - guard let self = self, let data = data else { return } + avatarRequest = ImageCache.avatars.get(account.avatar) { [weak self] (_, image) in + guard let self = self, let image = image else { return } self.avatarRequest = nil DispatchQueue.main.async { - self.avatarImageView.image = UIImage(data: data) + self.avatarImageView.image = image } } } diff --git a/Tusker/Views/AccountDisplayNameLabel.swift b/Tusker/Views/AccountDisplayNameLabel.swift index 06076a99..1e2368d9 100644 --- a/Tusker/Views/AccountDisplayNameLabel.swift +++ b/Tusker/Views/AccountDisplayNameLabel.swift @@ -54,9 +54,9 @@ struct AccountDisplayNameLabel: View { } group.enter() - let request = ImageCache.emojis.get(emoji.url) { (data) in + let request = ImageCache.emojis.get(emoji.url) { (_, image) in defer { group.leave() } - guard let data = data, let image = UIImage(data: data) else { return } + guard let image = image else { return } let size = CGSize(width: fontSize, height: fontSize) let renderer = UIGraphicsImageRenderer(size: size) diff --git a/Tusker/Views/Attachments/AttachmentView.swift b/Tusker/Views/Attachments/AttachmentView.swift index 61fa0440..6c4d4475 100644 --- a/Tusker/Views/Attachments/AttachmentView.swift +++ b/Tusker/Views/Attachments/AttachmentView.swift @@ -159,7 +159,7 @@ class AttachmentView: UIImageView, GIFAnimatable { func loadImage() { let attachmentURL = attachment.url - attachmentRequest = ImageCache.attachments.get(attachmentURL) { [weak self] (data) in + attachmentRequest = ImageCache.attachments.get(attachmentURL) { [weak self] (data, _) in guard let self = self, let data = data else { return } self.attachmentRequest = nil if self.attachment.url.pathExtension == "gif" { diff --git a/Tusker/Views/BaseEmojiLabel.swift b/Tusker/Views/BaseEmojiLabel.swift index 44510bbf..63b69a3b 100644 --- a/Tusker/Views/BaseEmojiLabel.swift +++ b/Tusker/Views/BaseEmojiLabel.swift @@ -43,20 +43,13 @@ extension BaseEmojiLabel { foundEmojis = true group.enter() - let request = ImageCache.emojis.get(emoji.url) { (data) in + let request = ImageCache.emojis.get(emoji.url) { (_, image) in defer { group.leave() } - guard let data = data else { + guard let image = image, + let transformedImage = ImageGrayscalifier.convertIfNecessary(url: emoji.url, image: image) else { return } - let image: UIImage? - if Preferences.shared.grayscaleImages { - image = ImageGrayscalifier.convert(url: emoji.url, data: data) - } else { - image = UIImage(data: data) - } - if let image = image { - emojiImages[emoji.shortcode] = image - } + emojiImages[emoji.shortcode] = transformedImage } if let request = request { emojiRequests.append(request) diff --git a/Tusker/Views/ContentTextView.swift b/Tusker/Views/ContentTextView.swift index 0777b83a..18425743 100644 --- a/Tusker/Views/ContentTextView.swift +++ b/Tusker/Views/ContentTextView.swift @@ -63,20 +63,13 @@ class ContentTextView: LinkTextView { for emoji in emojis { group.enter() - _ = ImageCache.emojis.get(emoji.url) { (data) in + _ = ImageCache.emojis.get(emoji.url) { (_, image) in defer { group.leave() } - guard let data = data else { + guard let image = image, + let transformedImage = ImageGrayscalifier.convertIfNecessary(url: emoji.url, image: image) else { return } - let image: UIImage? - if Preferences.shared.grayscaleImages { - image = ImageGrayscalifier.convert(url: emoji.url, data: data) - } else { - image = UIImage(data: data) - } - if let image = image { - emojiImages[emoji.shortcode] = image - } + emojiImages[emoji.shortcode] = transformedImage } } diff --git a/Tusker/Views/CustomEmojiImageView.swift b/Tusker/Views/CustomEmojiImageView.swift index bb72967b..30965912 100644 --- a/Tusker/Views/CustomEmojiImageView.swift +++ b/Tusker/Views/CustomEmojiImageView.swift @@ -33,16 +33,12 @@ struct CustomEmojiImageView: View { } private func loadImage() { - request = ImageCache.emojis.get(emoji.url) { (data) in - if let data = data, let image = UIImage(data: data) { - DispatchQueue.main.async { - self.request = nil + request = ImageCache.emojis.get(emoji.url) { (_, image) in + DispatchQueue.main.async { + self.request = nil + if let image = image { self.image = image } - } else { - DispatchQueue.main.async { - self.request = nil - } } } } diff --git a/Tusker/Views/Instance Cell/InstanceTableViewCell.swift b/Tusker/Views/Instance Cell/InstanceTableViewCell.swift index 55d890ae..f2a77e72 100644 --- a/Tusker/Views/Instance Cell/InstanceTableViewCell.swift +++ b/Tusker/Views/Instance Cell/InstanceTableViewCell.swift @@ -60,8 +60,8 @@ class InstanceTableViewCell: UITableViewCell { private func updateThumbnail(url: URL) { thumbnailImageView.image = nil thumbnailURL = url - 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 } + thumbnailRequest = ImageCache.attachments.get(url) { [weak self] (_, image) in + guard let self = self, self.thumbnailURL == url, let image = image else { return } self.thumbnailRequest = nil DispatchQueue.main.async { self.thumbnailImageView.image = image diff --git a/Tusker/Views/Notifications/ActionNotificationGroupTableViewCell.swift b/Tusker/Views/Notifications/ActionNotificationGroupTableViewCell.swift index 25ea06a0..81f2061c 100644 --- a/Tusker/Views/Notifications/ActionNotificationGroupTableViewCell.swift +++ b/Tusker/Views/Notifications/ActionNotificationGroupTableViewCell.swift @@ -84,21 +84,20 @@ class ActionNotificationGroupTableViewCell: UITableViewCell { imageView.layer.masksToBounds = true imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * 30 let avatarURL = account.avatar - avatarRequests[account.id] = ImageCache.avatars.get(avatarURL) { [weak self] (data) in - guard let self = self, let data = data, self.group.id == group.id else { return } - - let image: UIImage? - if self.isGrayscale { - image = ImageGrayscalifier.convert(url: avatarURL, data: data) - } else { - image = UIImage(data: data) - } - - if let image = image { + avatarRequests[account.id] = ImageCache.avatars.get(avatarURL) { [weak self] (_, image) in + guard let self = self else { return } + guard let image = image, + self.group.id == group.id, + let transformedImage = ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) else { DispatchQueue.main.async { self.avatarRequests.removeValue(forKey: account.id) - imageView.image = image } + return + } + + DispatchQueue.main.async { + self.avatarRequests.removeValue(forKey: account.id) + imageView.image = transformedImage } } actionAvatarStackView.addArrangedSubview(imageView) @@ -133,21 +132,20 @@ class ActionNotificationGroupTableViewCell: UITableViewCell { } let avatarURL = account.avatar - avatarRequests[account.id] = ImageCache.avatars.get(avatarURL) { [weak self] (data) in - guard let self = self, let data = data, self.group.id == groupID else { return } - - let image: UIImage? - if self.isGrayscale { - image = ImageGrayscalifier.convert(url: avatarURL, data: data) - } else { - image = UIImage(data: data) - } - - if let image = image { + avatarRequests[account.id] = ImageCache.avatars.get(avatarURL) { [weak self] (_, image) in + guard let self = self else { return } + guard let image = image, + self.group.id == groupID, + let transformedImage = ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) else { DispatchQueue.main.async { self.avatarRequests.removeValue(forKey: account.id) - imageView.image = image } + return + } + + DispatchQueue.main.async { + self.avatarRequests.removeValue(forKey: account.id) + imageView.image = transformedImage } } } diff --git a/Tusker/Views/Notifications/FollowNotificationGroupTableViewCell.swift b/Tusker/Views/Notifications/FollowNotificationGroupTableViewCell.swift index dd00926a..6181127e 100644 --- a/Tusker/Views/Notifications/FollowNotificationGroupTableViewCell.swift +++ b/Tusker/Views/Notifications/FollowNotificationGroupTableViewCell.swift @@ -65,21 +65,17 @@ class FollowNotificationGroupTableViewCell: UITableViewCell { imageView.layer.masksToBounds = true imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * 30 let avatarURL = account.avatar - avatarRequests[account.id] = ImageCache.avatars.get(avatarURL) { [weak self] (data) in - guard let self = self, let data = data, self.group.id == group.id else { return } - - let image: UIImage? - if Preferences.shared.grayscaleImages { - image = ImageGrayscalifier.convert(url: avatarURL, data: data) - } else { - image = UIImage(data: data) + avatarRequests[account.id] = ImageCache.avatars.get(avatarURL) { [weak self] (_, image) in + guard let self = self, + let image = image, + self.group.id == group.id, + let transformedImage = ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) else { + return } - if let image = image { - DispatchQueue.main.async { - self.avatarRequests.removeValue(forKey: account.id) - imageView.image = image - } + DispatchQueue.main.async { + self.avatarRequests.removeValue(forKey: account.id) + imageView.image = transformedImage } } avatarStackView.addArrangedSubview(imageView) @@ -103,21 +99,20 @@ class FollowNotificationGroupTableViewCell: UITableViewCell { } let avatarURL = account.avatar - avatarRequests[account.id] = ImageCache.avatars.get(avatarURL) { [weak self] (data) in - guard let self = self, let data = data, self.group.id == groupID else { return } - - let image: UIImage? - if self.isGrayscale { - image = ImageGrayscalifier.convert(url: avatarURL, data: data) - } else { - image = UIImage(data: data) - } - - if let image = image { + avatarRequests[account.id] = ImageCache.avatars.get(avatarURL) { [weak self] (_, image) in + guard let self = self else { return } + guard let image = image, + self.group.id == groupID, + let transformedImage = ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) else { DispatchQueue.main.async { self.avatarRequests.removeValue(forKey: account.id) - imageView.image = image } + return + } + + DispatchQueue.main.async { + self.avatarRequests.removeValue(forKey: account.id) + imageView.image = transformedImage } } } diff --git a/Tusker/Views/Notifications/FollowRequestNotificationTableViewCell.swift b/Tusker/Views/Notifications/FollowRequestNotificationTableViewCell.swift index b6fb0608..11783013 100644 --- a/Tusker/Views/Notifications/FollowRequestNotificationTableViewCell.swift +++ b/Tusker/Views/Notifications/FollowRequestNotificationTableViewCell.swift @@ -68,21 +68,18 @@ class FollowRequestNotificationTableViewCell: UITableViewCell { actionLabel.setEmojis(account.emojis, identifier: account.id) } let avatarURL = account.avatar - avatarRequest = ImageCache.avatars.get(avatarURL) { [weak self] (data) in - guard let self = self, self.account == account, let data = data else { return } + avatarRequest = ImageCache.avatars.get(avatarURL) { [weak self] (_, image) in + guard let self = self else { return } self.avatarRequest = nil - let image: UIImage? - if self.isGrayscale { - image = ImageGrayscalifier.convert(url: avatarURL, data: data) - } else { - image = UIImage(data: data) + guard self.account == account, + let image = image, + let transformedImage = ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) else { + return } - if let image = image { - DispatchQueue.main.async { - self.avatarImageView.image = image - } + DispatchQueue.main.async { + self.avatarImageView.image = transformedImage } } } diff --git a/Tusker/Views/Profile Header/ProfileHeaderView.swift b/Tusker/Views/Profile Header/ProfileHeaderView.swift index ab4c20ca..0e9fd4ea 100644 --- a/Tusker/Views/Profile Header/ProfileHeaderView.swift +++ b/Tusker/Views/Profile Header/ProfileHeaderView.swift @@ -191,35 +191,35 @@ class ProfileHeaderView: UIView { let accountID = account.id let avatarURL = account.avatar - avatarRequest = ImageCache.avatars.get(avatarURL) { [weak self] (data) in - guard let self = self, let data = data, self.accountID == accountID else { return } + avatarRequest = ImageCache.avatars.get(avatarURL) { [weak self] (_, image) in + guard let self = self, let image = image, self.accountID == accountID else { return } self.avatarRequest = nil - let image: UIImage? + let transformedImage: UIImage? if self.isGrayscale { - image = ImageGrayscalifier.convert(url: avatarURL, data: data) + transformedImage = ImageGrayscalifier.convert(url: avatarURL, cgImage: image.cgImage!) } else { - image = UIImage(data: data) + transformedImage = image } DispatchQueue.main.async { - self.avatarImageView.image = image + self.avatarImageView.image = transformedImage } } if let header = account.header { - headerRequest = ImageCache.headers.get(header) { [weak self] (data) in - guard let self = self, let data = data, self.accountID == accountID else { return } + headerRequest = ImageCache.headers.get(header) { [weak self] (_, image) in + guard let self = self, let image = image, self.accountID == accountID else { return } self.headerRequest = nil - let image: UIImage? + let transformedImage: UIImage? if self.isGrayscale { - image = ImageGrayscalifier.convert(url: header, data: data) + transformedImage = ImageGrayscalifier.convert(url: header, cgImage: image.cgImage!) } else { - image = UIImage(data: data) + transformedImage = image } DispatchQueue.main.async { - self.headerImageView.image = image + self.headerImageView.image = transformedImage } } } diff --git a/Tusker/Views/Status/BaseStatusTableViewCell.swift b/Tusker/Views/Status/BaseStatusTableViewCell.swift index 7e7af319..cde299e8 100644 --- a/Tusker/Views/Status/BaseStatusTableViewCell.swift +++ b/Tusker/Views/Status/BaseStatusTableViewCell.swift @@ -260,18 +260,14 @@ class BaseStatusTableViewCell: UITableViewCell { let avatarURL = account.avatar let accountID = account.id - avatarRequest = ImageCache.avatars.get(avatarURL) { [weak self] (data) in - guard let self = self, let data = data, self.accountID == accountID else { return } - - let image: UIImage? - if self.isGrayscale { - image = ImageGrayscalifier.convert(url: avatarURL, data: data) - } else { - image = UIImage(data: data) - } + avatarRequest = ImageCache.avatars.get(avatarURL) { [weak self] (_, image) in + guard let self = self, + let image = image, + self.accountID == accountID, + let transformedImage = ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) else { return } DispatchQueue.main.async { - self.avatarImageView.image = image + self.avatarImageView.image = transformedImage } } diff --git a/Tusker/Views/Status/StatusCardView.swift b/Tusker/Views/Status/StatusCardView.swift index c60be175..a6505ede 100644 --- a/Tusker/Views/Status/StatusCardView.swift +++ b/Tusker/Views/Status/StatusCardView.swift @@ -141,18 +141,13 @@ class StatusCardView: UIView { if let imageURL = card.image { placeholderImageView.isHidden = true - imageRequest = ImageCache.attachments.get(imageURL, completion: { (data) in - guard let data = data else { return } - let image: UIImage? - if self.isGrayscale { - image = ImageGrayscalifier.convert(url: imageURL, data: data) - } else { - image = UIImage(data: data) + imageRequest = ImageCache.attachments.get(imageURL, completion: { (_, image) in + guard let image = image, + let transformedImage = ImageGrayscalifier.convertIfNecessary(url: imageURL, image: image) else { + return } - if let image = image { - DispatchQueue.main.async { - self.imageView.image = image - } + DispatchQueue.main.async { + self.imageView.image = transformedImage } }) if imageRequest != nil {