Cache UIImage objects to avoid re-decoding images unnecessarily

This commit is contained in:
Shadowfacts 2021-01-16 15:24:15 -05:00
parent 27b39b79e6
commit c12d2db258
28 changed files with 243 additions and 223 deletions

View File

@ -112,6 +112,7 @@
D62D2424217ABF3F005076CC /* NSUserActivity+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2423217ABF3F005076CC /* NSUserActivity+Extensions.swift */; }; D62D2424217ABF3F005076CC /* NSUserActivity+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2423217ABF3F005076CC /* NSUserActivity+Extensions.swift */; };
D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2425217ABF63005076CC /* UserActivityType.swift */; }; D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2425217ABF63005076CC /* UserActivityType.swift */; };
D62FF04823D7CDD700909D6E /* AttributedStringHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62FF04723D7CDD700909D6E /* AttributedStringHelperTests.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 */; }; 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 */; }; D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */; };
D63569E023908A8D003DD353 /* StatusState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60A4FFB238B726A008AC647 /* StatusState.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 = "<group>"; }; D62D2423217ABF3F005076CC /* NSUserActivity+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSUserActivity+Extensions.swift"; sourceTree = "<group>"; };
D62D2425217ABF63005076CC /* UserActivityType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivityType.swift; sourceTree = "<group>"; }; D62D2425217ABF63005076CC /* UserActivityType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivityType.swift; sourceTree = "<group>"; };
D62FF04723D7CDD700909D6E /* AttributedStringHelperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringHelperTests.swift; sourceTree = "<group>"; }; D62FF04723D7CDD700909D6E /* AttributedStringHelperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringHelperTests.swift; sourceTree = "<group>"; };
D6311C4F25B3765B00B27539 /* ImageDataCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDataCache.swift; sourceTree = "<group>"; };
D6333B362137838300CE884A /* AttributedString+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttributedString+Helpers.swift"; sourceTree = "<group>"; }; D6333B362137838300CE884A /* AttributedString+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttributedString+Helpers.swift"; sourceTree = "<group>"; };
D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+TimeAgo.swift"; sourceTree = "<group>"; }; D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+TimeAgo.swift"; sourceTree = "<group>"; };
D63661BF2381C144004B9E16 /* PreferencesNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesNavigationController.swift; sourceTree = "<group>"; }; D63661BF2381C144004B9E16 /* PreferencesNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesNavigationController.swift; sourceTree = "<group>"; };
@ -1493,6 +1495,7 @@
children = ( children = (
D6F1F84C2193B56E00F5FE67 /* Cache.swift */, D6F1F84C2193B56E00F5FE67 /* Cache.swift */,
04DACE8D212CC7CC009840C4 /* ImageCache.swift */, 04DACE8D212CC7CC009840C4 /* ImageCache.swift */,
D6311C4F25B3765B00B27539 /* ImageDataCache.swift */,
); );
path = Caching; path = Caching;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1889,6 +1892,7 @@
D627943723A552C200D38C68 /* BookmarkStatusActivity.swift in Sources */, D627943723A552C200D38C68 /* BookmarkStatusActivity.swift in Sources */,
D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */, D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */,
D6AC956723C4347E008C9946 /* MainSceneDelegate.swift in Sources */, D6AC956723C4347E008C9946 /* MainSceneDelegate.swift in Sources */,
D6311C5025B3765B00B27539 /* ImageDataCache.swift in Sources */,
D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */, D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */,
D60E2F2E244248BF005F8713 /* MastodonCachePersistentStore.swift in Sources */, D60E2F2E244248BF005F8713 /* MastodonCachePersistentStore.swift in Sources */,
D620483623D38075008A63EF /* ContentTextView.swift in Sources */, D620483623D38075008A63EF /* ContentTextView.swift in Sources */,

View File

@ -29,7 +29,7 @@ class AccountActivityItemSource: NSObject, UIActivityItemSource {
metadata.originalURL = account.url metadata.originalURL = account.url
metadata.url = account.url metadata.url = account.url
metadata.title = "\(account.displayName) (@\(account.username)@\(account.url.host!)" 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) { let image = UIImage(data: data) {
metadata.iconProvider = NSItemProvider(object: image) metadata.iconProvider = NSItemProvider(object: image)
} }

View File

@ -32,7 +32,7 @@ class StatusActivityItemSource: NSObject, UIActivityItemSource {
let doc = try! SwiftSoup.parse(status.content) let doc = try! SwiftSoup.parse(status.content)
let content = try! doc.text() let content = try! doc.text()
metadata.title = "\(status.account.displayName): \"\(content)\"" 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) { let image = UIImage(data: data) {
metadata.iconProvider = NSItemProvider(object: image) metadata.iconProvider = NSItemProvider(object: image)
} }

View File

@ -22,47 +22,36 @@ class ImageCache {
private static let disableCaching = false private static let disableCaching = false
#endif #endif
private let cache: Cache<Data> private let cache: ImageDataCache
private var groups = MultiThreadDictionary<URL, RequestGroup>(name: "ImageCache request groups") private var groups = MultiThreadDictionary<URL, RequestGroup>(name: "ImageCache request groups")
private var backgroundQueue = DispatchQueue(label: "ImageCache completion queue", qos: .default) private var backgroundQueue = DispatchQueue(label: "ImageCache completion queue", qos: .default)
init(name: String, memoryExpiry expiry: Expiry) { init(name: String, memoryExpiry: Expiry, diskExpiry: Expiry? = nil) {
let storage = MemoryStorage<Data>(config: MemoryConfig(expiry: expiry)) self.cache = ImageDataCache(name: name, memoryExpiry: memoryExpiry, diskExpiry: diskExpiry)
self.cache = .memory(storage)
} }
init(name: String, diskExpiry expiry: Expiry) { func get(_ url: URL, completion: ((Data?, UIImage?) -> Void)?) -> Request? {
let storage = try! DiskStorage<Data>(config: DiskConfig(name: name, expiry: expiry), transformer: TransformerFactory.forData())
self.cache = .disk(storage)
}
init(name: String, memoryExpiry: Expiry, diskExpiry: Expiry) {
let memory = MemoryStorage<Data>(config: MemoryConfig(expiry: memoryExpiry))
let disk = try! DiskStorage<Data>(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? {
let key = url.absoluteString let key = url.absoluteString
if !ImageCache.disableCaching, if !ImageCache.disableCaching,
// todo: calling object(forKey: key) does disk I/O and this method is often called from the main thread // 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 // 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 // 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 // path can be avoided most of the time
let data = try? cache.object(forKey: key) { let (data, image) = try? cache.get(key) {
backgroundQueue.async { backgroundQueue.async {
completion?(data) completion?(data, image)
} }
return nil return nil
} else { } else {
if let completion = completion, let group = groups[url] { if let completion = completion, let group = groups[url] {
return group.addCallback(completion) return group.addCallback(completion)
} else { } else {
let group = RequestGroup(url: url) { (data) in let group = RequestGroup(url: url) { (data, image) in
if let data = data { if let data = data,
try? self.cache.setObject(data, forKey: key) let image = UIImage(data: data) {
try? self.cache.set(key, data: data, image: image)
} }
self.groups.removeValueWithoutReturning(forKey: url) self.groups.removeValueWithoutReturning(forKey: url)
} }
@ -74,8 +63,12 @@ class ImageCache {
} }
} }
func get(_ url: URL) -> Data? { func getData(_ url: URL) -> Data? {
return try? cache.object(forKey: url.absoluteString) return try? cache.getData(url.absoluteString)
}
func get(_ url: URL) -> (Data, UIImage)? {
return try? cache.get(url.absoluteString)
} }
func cancelWithoutCallback(_ url: URL) { func cancelWithoutCallback(_ url: URL) {
@ -88,11 +81,11 @@ class ImageCache {
private class RequestGroup { private class RequestGroup {
let url: URL let url: URL
private let onFinished: (Data?) -> Void private let onFinished: (Data?, UIImage?) -> Void
private var task: URLSessionDataTask? private var task: URLSessionDataTask?
private var requests = [Request]() private var requests = [Request]()
init(url: URL, onFinished: @escaping (Data?) -> Void) { init(url: URL, onFinished: @escaping (Data?, UIImage?) -> Void) {
self.url = url self.url = url
self.onFinished = onFinished self.onFinished = onFinished
} }
@ -116,7 +109,7 @@ class ImageCache {
task?.priority = max(1.0, URLSessionTask.defaultPriority + 0.1 * Float(requests.filter { !$0.cancelled }.count)) 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) let request = Request(callback: completion)
requests.append(request) requests.append(request)
updatePriority() updatePriority()
@ -141,21 +134,24 @@ class ImageCache {
} }
func complete(with data: Data?) { func complete(with data: Data?) {
let image = data != nil ? UIImage(data: data!) : nil
requests.filter { !$0.cancelled }.forEach { requests.filter { !$0.cancelled }.forEach {
if let callback = $0.callback { if let callback = $0.callback {
callback(data) callback(data, image)
} }
} }
self.onFinished(data)
self.onFinished(data, image)
} }
} }
class Request { class Request {
private weak var group: RequestGroup? private weak var group: RequestGroup?
private(set) var callback: ((Data?) -> Void)? private(set) var callback: ((Data?, UIImage?) -> Void)?
private(set) var cancelled: Bool = false private(set) var cancelled: Bool = false
init(callback: ((Data?) -> Void)?) { init(callback: ((Data?, UIImage?) -> Void)?) {
self.callback = callback self.callback = callback
} }

View File

@ -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<Data>?
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()
}
}

View File

@ -14,6 +14,15 @@ struct ImageGrayscalifier {
private static let context = CIContext() private static let context = CIContext()
private static let cache = NSCache<NSURL, UIImage>() private static let cache = NSCache<NSURL, UIImage>()
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? { static func convert(url: URL?, data: Data) -> UIImage? {
if let url = url, if let url = url,
let cached = cache.object(forKey: url as NSURL) { let cached = cache.object(forKey: url as NSURL) {

View File

@ -25,7 +25,7 @@ class AttachmentPreviewViewController: UIViewController {
} }
override func loadView() { 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 image = UIImage(data: data) {
let imageView: UIImageView let imageView: UIImageView
if attachment.url.pathExtension == "gif" { if attachment.url.pathExtension == "gif" {

View File

@ -44,7 +44,7 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc
var animationGifData: Data? { var animationGifData: Data? {
let attachment = attachments[currentIndex] let attachment = attachments[currentIndex]
if attachment.url.pathExtension == "gif" { if attachment.url.pathExtension == "gif" {
return ImageCache.attachments.get(attachment.url) return ImageCache.attachments.getData(attachment.url)
} else { } else {
return nil return nil
} }

View File

@ -44,17 +44,11 @@ struct ComposeAvatarImageView: View {
private func loadImage() { private func loadImage() {
guard let url = url else { return } guard let url = url else { return }
request = ImageCache.avatars.get(url) { (data) in request = ImageCache.avatars.get(url) { (_, image) in
if let data = data, let image = UIImage(data: data) {
DispatchQueue.main.async { DispatchQueue.main.async {
self.request = nil self.request = nil
self.avatarImage = image self.avatarImage = image
} }
} else {
DispatchQueue.main.async {
self.request = nil
}
}
} }
} }

View File

@ -45,15 +45,14 @@ class EmojiCollectionViewCell: UICollectionViewCell {
func updateUI(emoji: Emoji) { func updateUI(emoji: Emoji) {
currentEmojiShortcode = emoji.shortcode currentEmojiShortcode = emoji.shortcode
imageRequest = ImageCache.emojis.get(emoji.url) { [weak self] (data) in imageRequest = ImageCache.emojis.get(emoji.url) { [weak self] (_, image) in
if let data = data, let image = UIImage(data: data) { guard let image = image else { return }
DispatchQueue.main.async { [weak self] in DispatchQueue.main.async { [weak self] in
guard let self = self, self.currentEmojiShortcode == emoji.shortcode else { return } guard let self = self, self.currentEmojiShortcode == emoji.shortcode else { return }
self.emojiImageView.image = image self.emojiImageView.image = image
} }
} }
} }
}
override func prepareForReuse() { override func prepareForReuse() {
super.prepareForReuse() super.prepareForReuse()

View File

@ -87,8 +87,8 @@ class FastSwitchingAccountView: UIView {
let controller = MastodonController.getForAccount(account) let controller = MastodonController.getForAccount(account)
controller.getOwnAccount { [weak self] (result) in controller.getOwnAccount { [weak self] (result) in
guard let self = self, case let .success(account) = result else { return } guard let self = self, case let .success(account) = result else { return }
self.avatarRequest = ImageCache.avatars.get(account.avatar) { [weak avatarImageView] (data) in self.avatarRequest = ImageCache.avatars.get(account.avatar) { [weak avatarImageView] (_, image) in
guard let avatarImageView = avatarImageView, let data = data, let image = UIImage(data: data) else { return } guard let avatarImageView = avatarImageView, let image = image else { return }
DispatchQueue.main.async { DispatchQueue.main.async {
avatarImageView.image = image avatarImageView.image = image
} }

View File

@ -85,19 +85,19 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
overrideUserInterfaceStyle = .dark overrideUserInterfaceStyle = .dark
view.backgroundColor = .black view.backgroundColor = .black
if let data = cache.get(url) { if let (data, image) = cache.get(url) {
createLargeImage(data: data, url: url) createLargeImage(data: data, image: image, url: url)
} else { } else {
createPreview() createPreview()
loadingVC = LoadingViewController() loadingVC = LoadingViewController()
embedChild(loadingVC!) 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 } guard let self = self else { return }
self.imageRequest = nil self.imageRequest = nil
DispatchQueue.main.async { DispatchQueue.main.async {
self.loadingVC?.removeViewAndController() 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 } guard !loaded else { return }
loaded = true loaded = true
let image: UIImage? if let transformedImage = ImageGrayscalifier.convertIfNecessary(url: url, image: image) {
if Preferences.shared.grayscaleImages {
image = ImageGrayscalifier.convert(url: url, data: data)
} else {
image = UIImage(data: data)
}
if let image = image {
let gifData = url.pathExtension == "gif" ? data : nil let gifData = url.pathExtension == "gif" ? data : nil
createLargeImage(image: image, gifData: gifData) createLargeImage(image: transformedImage, gifData: gifData)
} }
} }

View File

@ -38,15 +38,13 @@ struct LocalAccountAvatarView: View {
let controller = MastodonController.getForAccount(localAccountInfo) let controller = MastodonController.getForAccount(localAccountInfo)
controller.getOwnAccount { (result) in controller.getOwnAccount { (result) in
guard case let .success(account) = result else { return } guard case let .success(account) = result else { return }
_ = ImageCache.avatars.get(account.avatar) { (data) in _ = ImageCache.avatars.get(account.avatar) { (_, image) in
if let data = data, let image = UIImage(data: data) {
DispatchQueue.main.async { DispatchQueue.main.async {
self.avatarImage = image self.avatarImage = image
} }
} }
} }
} }
}
} }
//struct LocalAccountAvatarView_Previews: PreviewProvider { //struct LocalAccountAvatarView_Previews: PreviewProvider {

View File

@ -43,17 +43,10 @@ class MyProfileViewController: ProfileViewController {
private func setAvatarTabBarImage<Account: AccountProtocol>(account: Account) { private func setAvatarTabBarImage<Account: AccountProtocol>(account: Account) {
let avatarURL = account.avatar let avatarURL = account.avatar
_ = ImageCache.avatars.get(avatarURL, completion: { [weak self] (data) in _ = ImageCache.avatars.get(avatarURL, completion: { [weak self] (_, image) in
guard let self = self, let data = data else { return } guard let self = self,
let image = image,
let maybeGrayscale: UIImage? let maybeGrayscale = ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) else {
if Preferences.shared.grayscaleImages {
maybeGrayscale = ImageGrayscalifier.convert(url: avatarURL, data: data)
} else {
maybeGrayscale = UIImage(data: data)
}
guard let image = maybeGrayscale else {
return return
} }
@ -63,7 +56,7 @@ class MyProfileViewController: ProfileViewController {
let tabBarImage = UIGraphicsImageRenderer(size: size).image { (_) in let tabBarImage = UIGraphicsImageRenderer(size: size).image { (_) in
let radius = Preferences.shared.avatarStyle.cornerRadiusFraction * 30 let radius = Preferences.shared.avatarStyle.cornerRadiusFraction * 30
UIBezierPath(roundedRect: rect, cornerRadius: radius).addClip() UIBezierPath(roundedRect: rect, cornerRadius: radius).addClip()
image.draw(in: rect) maybeGrayscale.draw(in: rect)
} }
let alwaysOriginalImage = tabBarImage.withRenderingMode(.alwaysOriginal) let alwaysOriginalImage = tabBarImage.withRenderingMode(.alwaysOriginal)
self.tabBarItem.image = alwaysOriginalImage self.tabBarItem.image = alwaysOriginalImage

View File

@ -63,19 +63,16 @@ class AccountTableViewCell: UITableViewCell {
let accountID = self.accountID let accountID = self.accountID
let avatarURL = account.avatar let avatarURL = account.avatar
avatarRequest = ImageCache.avatars.get(avatarURL) { [weak self] (data) in avatarRequest = ImageCache.avatars.get(avatarURL) { [weak self] (_, image) in
guard let self = self, let data = data, self.accountID == accountID else { return } guard let self = self else { return }
self.avatarRequest = nil self.avatarRequest = nil
let image: UIImage? guard let image = image,
if self.isGrayscale { self.accountID == accountID,
image = ImageGrayscalifier.convert(url: avatarURL, data: data) let transformedImage = ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) else { return }
} else {
image = UIImage(data: data)
}
DispatchQueue.main.async { DispatchQueue.main.async {
self.avatarImageView.image = image self.avatarImageView.image = transformedImage
} }
} }

View File

@ -69,11 +69,11 @@ class LargeAccountDetailView: UIView {
displayNameLabel.updateForAccountDisplayName(account: account) displayNameLabel.updateForAccountDisplayName(account: account)
usernameLabel.text = "@\(account.acct)" usernameLabel.text = "@\(account.acct)"
avatarRequest = ImageCache.avatars.get(account.avatar) { [weak self] (data) in avatarRequest = ImageCache.avatars.get(account.avatar) { [weak self] (_, image) in
guard let self = self, let data = data else { return } guard let self = self, let image = image else { return }
self.avatarRequest = nil self.avatarRequest = nil
DispatchQueue.main.async { DispatchQueue.main.async {
self.avatarImageView.image = UIImage(data: data) self.avatarImageView.image = image
} }
} }
} }

View File

@ -54,9 +54,9 @@ struct AccountDisplayNameLabel<Account: AccountProtocol>: View {
} }
group.enter() group.enter()
let request = ImageCache.emojis.get(emoji.url) { (data) in let request = ImageCache.emojis.get(emoji.url) { (_, image) in
defer { group.leave() } 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 size = CGSize(width: fontSize, height: fontSize)
let renderer = UIGraphicsImageRenderer(size: size) let renderer = UIGraphicsImageRenderer(size: size)

View File

@ -159,7 +159,7 @@ class AttachmentView: UIImageView, GIFAnimatable {
func loadImage() { func loadImage() {
let attachmentURL = attachment.url 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 } guard let self = self, let data = data else { return }
self.attachmentRequest = nil self.attachmentRequest = nil
if self.attachment.url.pathExtension == "gif" { if self.attachment.url.pathExtension == "gif" {

View File

@ -43,20 +43,13 @@ extension BaseEmojiLabel {
foundEmojis = true foundEmojis = true
group.enter() group.enter()
let request = ImageCache.emojis.get(emoji.url) { (data) in let request = ImageCache.emojis.get(emoji.url) { (_, image) in
defer { group.leave() } defer { group.leave() }
guard let data = data else { guard let image = image,
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: emoji.url, image: image) else {
return return
} }
let image: UIImage? emojiImages[emoji.shortcode] = transformedImage
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
}
} }
if let request = request { if let request = request {
emojiRequests.append(request) emojiRequests.append(request)

View File

@ -63,20 +63,13 @@ class ContentTextView: LinkTextView {
for emoji in emojis { for emoji in emojis {
group.enter() group.enter()
_ = ImageCache.emojis.get(emoji.url) { (data) in _ = ImageCache.emojis.get(emoji.url) { (_, image) in
defer { group.leave() } defer { group.leave() }
guard let data = data else { guard let image = image,
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: emoji.url, image: image) else {
return return
} }
let image: UIImage? emojiImages[emoji.shortcode] = transformedImage
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
}
} }
} }

View File

@ -33,16 +33,12 @@ struct CustomEmojiImageView: View {
} }
private func loadImage() { private func loadImage() {
request = ImageCache.emojis.get(emoji.url) { (data) in request = ImageCache.emojis.get(emoji.url) { (_, image) in
if let data = data, let image = UIImage(data: data) {
DispatchQueue.main.async { DispatchQueue.main.async {
self.request = nil self.request = nil
if let image = image {
self.image = image self.image = image
} }
} else {
DispatchQueue.main.async {
self.request = nil
}
} }
} }
} }

View File

@ -60,8 +60,8 @@ class InstanceTableViewCell: UITableViewCell {
private func updateThumbnail(url: URL) { private func updateThumbnail(url: URL) {
thumbnailImageView.image = nil thumbnailImageView.image = nil
thumbnailURL = url thumbnailURL = url
thumbnailRequest = ImageCache.attachments.get(url) { [weak self] (data) in thumbnailRequest = ImageCache.attachments.get(url) { [weak self] (_, image) in
guard let self = self, self.thumbnailURL == url, let data = data, let image = UIImage(data: data) else { return } guard let self = self, self.thumbnailURL == url, let image = image else { return }
self.thumbnailRequest = nil self.thumbnailRequest = nil
DispatchQueue.main.async { DispatchQueue.main.async {
self.thumbnailImageView.image = image self.thumbnailImageView.image = image

View File

@ -84,21 +84,20 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
imageView.layer.masksToBounds = true imageView.layer.masksToBounds = true
imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * 30 imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * 30
let avatarURL = account.avatar let avatarURL = account.avatar
avatarRequests[account.id] = ImageCache.avatars.get(avatarURL) { [weak self] (data) in avatarRequests[account.id] = ImageCache.avatars.get(avatarURL) { [weak self] (_, image) in
guard let self = self, let data = data, self.group.id == group.id else { return } guard let self = self else { return }
guard let image = image,
let image: UIImage? self.group.id == group.id,
if self.isGrayscale { let transformedImage = ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) else {
image = ImageGrayscalifier.convert(url: avatarURL, data: data)
} else {
image = UIImage(data: data)
}
if let image = image {
DispatchQueue.main.async { DispatchQueue.main.async {
self.avatarRequests.removeValue(forKey: account.id) 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) actionAvatarStackView.addArrangedSubview(imageView)
@ -133,21 +132,20 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
} }
let avatarURL = account.avatar let avatarURL = account.avatar
avatarRequests[account.id] = ImageCache.avatars.get(avatarURL) { [weak self] (data) in avatarRequests[account.id] = ImageCache.avatars.get(avatarURL) { [weak self] (_, image) in
guard let self = self, let data = data, self.group.id == groupID else { return } guard let self = self else { return }
guard let image = image,
let image: UIImage? self.group.id == groupID,
if self.isGrayscale { let transformedImage = ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) else {
image = ImageGrayscalifier.convert(url: avatarURL, data: data)
} else {
image = UIImage(data: data)
}
if let image = image {
DispatchQueue.main.async { DispatchQueue.main.async {
self.avatarRequests.removeValue(forKey: account.id) self.avatarRequests.removeValue(forKey: account.id)
imageView.image = image
} }
return
}
DispatchQueue.main.async {
self.avatarRequests.removeValue(forKey: account.id)
imageView.image = transformedImage
} }
} }
} }

View File

@ -65,21 +65,17 @@ class FollowNotificationGroupTableViewCell: UITableViewCell {
imageView.layer.masksToBounds = true imageView.layer.masksToBounds = true
imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * 30 imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * 30
let avatarURL = account.avatar let avatarURL = account.avatar
avatarRequests[account.id] = ImageCache.avatars.get(avatarURL) { [weak self] (data) in avatarRequests[account.id] = ImageCache.avatars.get(avatarURL) { [weak self] (_, image) in
guard let self = self, let data = data, self.group.id == group.id else { return } guard let self = self,
let image = image,
let image: UIImage? self.group.id == group.id,
if Preferences.shared.grayscaleImages { let transformedImage = ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) else {
image = ImageGrayscalifier.convert(url: avatarURL, data: data) return
} else {
image = UIImage(data: data)
} }
if let image = image {
DispatchQueue.main.async { DispatchQueue.main.async {
self.avatarRequests.removeValue(forKey: account.id) self.avatarRequests.removeValue(forKey: account.id)
imageView.image = image imageView.image = transformedImage
}
} }
} }
avatarStackView.addArrangedSubview(imageView) avatarStackView.addArrangedSubview(imageView)
@ -103,21 +99,20 @@ class FollowNotificationGroupTableViewCell: UITableViewCell {
} }
let avatarURL = account.avatar let avatarURL = account.avatar
avatarRequests[account.id] = ImageCache.avatars.get(avatarURL) { [weak self] (data) in avatarRequests[account.id] = ImageCache.avatars.get(avatarURL) { [weak self] (_, image) in
guard let self = self, let data = data, self.group.id == groupID else { return } guard let self = self else { return }
guard let image = image,
let image: UIImage? self.group.id == groupID,
if self.isGrayscale { let transformedImage = ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) else {
image = ImageGrayscalifier.convert(url: avatarURL, data: data)
} else {
image = UIImage(data: data)
}
if let image = image {
DispatchQueue.main.async { DispatchQueue.main.async {
self.avatarRequests.removeValue(forKey: account.id) self.avatarRequests.removeValue(forKey: account.id)
imageView.image = image
} }
return
}
DispatchQueue.main.async {
self.avatarRequests.removeValue(forKey: account.id)
imageView.image = transformedImage
} }
} }
} }

View File

@ -68,21 +68,18 @@ class FollowRequestNotificationTableViewCell: UITableViewCell {
actionLabel.setEmojis(account.emojis, identifier: account.id) actionLabel.setEmojis(account.emojis, identifier: account.id)
} }
let avatarURL = account.avatar let avatarURL = account.avatar
avatarRequest = ImageCache.avatars.get(avatarURL) { [weak self] (data) in avatarRequest = ImageCache.avatars.get(avatarURL) { [weak self] (_, image) in
guard let self = self, self.account == account, let data = data else { return } guard let self = self else { return }
self.avatarRequest = nil self.avatarRequest = nil
let image: UIImage? guard self.account == account,
if self.isGrayscale { let image = image,
image = ImageGrayscalifier.convert(url: avatarURL, data: data) let transformedImage = ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) else {
} else { return
image = UIImage(data: data)
} }
if let image = image {
DispatchQueue.main.async { DispatchQueue.main.async {
self.avatarImageView.image = image self.avatarImageView.image = transformedImage
}
} }
} }
} }

View File

@ -191,35 +191,35 @@ class ProfileHeaderView: UIView {
let accountID = account.id let accountID = account.id
let avatarURL = account.avatar let avatarURL = account.avatar
avatarRequest = ImageCache.avatars.get(avatarURL) { [weak self] (data) in avatarRequest = ImageCache.avatars.get(avatarURL) { [weak self] (_, image) in
guard let self = self, let data = data, self.accountID == accountID else { return } guard let self = self, let image = image, self.accountID == accountID else { return }
self.avatarRequest = nil self.avatarRequest = nil
let image: UIImage? let transformedImage: UIImage?
if self.isGrayscale { if self.isGrayscale {
image = ImageGrayscalifier.convert(url: avatarURL, data: data) transformedImage = ImageGrayscalifier.convert(url: avatarURL, cgImage: image.cgImage!)
} else { } else {
image = UIImage(data: data) transformedImage = image
} }
DispatchQueue.main.async { DispatchQueue.main.async {
self.avatarImageView.image = image self.avatarImageView.image = transformedImage
} }
} }
if let header = account.header { if let header = account.header {
headerRequest = ImageCache.headers.get(header) { [weak self] (data) in headerRequest = ImageCache.headers.get(header) { [weak self] (_, image) in
guard let self = self, let data = data, self.accountID == accountID else { return } guard let self = self, let image = image, self.accountID == accountID else { return }
self.headerRequest = nil self.headerRequest = nil
let image: UIImage? let transformedImage: UIImage?
if self.isGrayscale { if self.isGrayscale {
image = ImageGrayscalifier.convert(url: header, data: data) transformedImage = ImageGrayscalifier.convert(url: header, cgImage: image.cgImage!)
} else { } else {
image = UIImage(data: data) transformedImage = image
} }
DispatchQueue.main.async { DispatchQueue.main.async {
self.headerImageView.image = image self.headerImageView.image = transformedImage
} }
} }
} }

View File

@ -260,18 +260,14 @@ class BaseStatusTableViewCell: UITableViewCell {
let avatarURL = account.avatar let avatarURL = account.avatar
let accountID = account.id let accountID = account.id
avatarRequest = ImageCache.avatars.get(avatarURL) { [weak self] (data) in avatarRequest = ImageCache.avatars.get(avatarURL) { [weak self] (_, image) in
guard let self = self, let data = data, self.accountID == accountID else { return } guard let self = self,
let image = image,
let image: UIImage? self.accountID == accountID,
if self.isGrayscale { let transformedImage = ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) else { return }
image = ImageGrayscalifier.convert(url: avatarURL, data: data)
} else {
image = UIImage(data: data)
}
DispatchQueue.main.async { DispatchQueue.main.async {
self.avatarImageView.image = image self.avatarImageView.image = transformedImage
} }
} }

View File

@ -141,18 +141,13 @@ class StatusCardView: UIView {
if let imageURL = card.image { if let imageURL = card.image {
placeholderImageView.isHidden = true placeholderImageView.isHidden = true
imageRequest = ImageCache.attachments.get(imageURL, completion: { (data) in imageRequest = ImageCache.attachments.get(imageURL, completion: { (_, image) in
guard let data = data else { return } guard let image = image,
let image: UIImage? let transformedImage = ImageGrayscalifier.convertIfNecessary(url: imageURL, image: image) else {
if self.isGrayscale { return
image = ImageGrayscalifier.convert(url: imageURL, data: data)
} else {
image = UIImage(data: data)
} }
if let image = image {
DispatchQueue.main.async { DispatchQueue.main.async {
self.imageView.image = image self.imageView.image = transformedImage
}
} }
}) })
if imageRequest != nil { if imageRequest != nil {