Refactor AvatarCache to ImageCache

Use Cache library (https://github.com/hyperoslo/Cache) for caching
This commit is contained in:
Shadowfacts 2018-10-02 13:42:37 -04:00
parent 46d4469df9
commit c69d3e2962
12 changed files with 115 additions and 78 deletions

3
.gitmodules vendored
View File

@ -1,3 +1,6 @@
[submodule "SwiftSoup"] [submodule "SwiftSoup"]
path = SwiftSoup path = SwiftSoup
url = git://github.com/scinfu/SwiftSoup.git url = git://github.com/scinfu/SwiftSoup.git
[submodule "Cache"]
path = Cache
url = git@github.com:hyperoslo/Cache.git

1
Cache Submodule

@ -0,0 +1 @@
Subproject commit 8c42c575cf28b2ff0e780c9728721e9a8891c92e

View File

@ -12,8 +12,10 @@
04496BD0216252E5001F1B23 /* TTTAttributedLabel.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 04496BC8216252E5001F1B23 /* TTTAttributedLabel.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 04496BD0216252E5001F1B23 /* TTTAttributedLabel.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 04496BC8216252E5001F1B23 /* TTTAttributedLabel.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
04496BD52162530A001F1B23 /* TTTAttributedLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = 04496BD42162530A001F1B23 /* TTTAttributedLabel.m */; }; 04496BD52162530A001F1B23 /* TTTAttributedLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = 04496BD42162530A001F1B23 /* TTTAttributedLabel.m */; };
04496BD721625361001F1B23 /* ContentLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04496BD621625361001F1B23 /* ContentLabel.swift */; }; 04496BD721625361001F1B23 /* ContentLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04496BD621625361001F1B23 /* ContentLabel.swift */; };
0461A3902163CBAE00C0A807 /* Cache.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0461A38F2163CBAE00C0A807 /* Cache.framework */; };
0461A3912163CBAE00C0A807 /* Cache.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 0461A38F2163CBAE00C0A807 /* Cache.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */; }; 04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */; };
04DACE8E212CC7CC009840C4 /* AvatarCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8D212CC7CC009840C4 /* AvatarCache.swift */; }; 04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8D212CC7CC009840C4 /* ImageCache.swift */; };
04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04ED00B021481ED800567C53 /* SteppedProgressView.swift */; }; 04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04ED00B021481ED800567C53 /* SteppedProgressView.swift */; };
D6028B9B2150811100F223B9 /* MastodonCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6028B9A2150811100F223B9 /* MastodonCache.swift */; }; D6028B9B2150811100F223B9 /* MastodonCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6028B9A2150811100F223B9 /* MastodonCache.swift */; };
D61099B42144B0CC00432DC2 /* Pachyderm.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D61099AB2144B0CC00432DC2 /* Pachyderm.framework */; }; D61099B42144B0CC00432DC2 /* Pachyderm.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D61099AB2144B0CC00432DC2 /* Pachyderm.framework */; };
@ -178,6 +180,7 @@
04496BD0216252E5001F1B23 /* TTTAttributedLabel.framework in Embed Frameworks */, 04496BD0216252E5001F1B23 /* TTTAttributedLabel.framework in Embed Frameworks */,
D61099C12144B0CC00432DC2 /* Pachyderm.framework in Embed Frameworks */, D61099C12144B0CC00432DC2 /* Pachyderm.framework in Embed Frameworks */,
D6BED170212663DA00F02DA0 /* SwiftSoup.framework in Embed Frameworks */, D6BED170212663DA00F02DA0 /* SwiftSoup.framework in Embed Frameworks */,
0461A3912163CBAE00C0A807 /* Cache.framework in Embed Frameworks */,
); );
name = "Embed Frameworks"; name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@ -190,8 +193,9 @@
04496BCB216252E5001F1B23 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 04496BCB216252E5001F1B23 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
04496BD42162530A001F1B23 /* TTTAttributedLabel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TTTAttributedLabel.m; sourceTree = "<group>"; }; 04496BD42162530A001F1B23 /* TTTAttributedLabel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TTTAttributedLabel.m; sourceTree = "<group>"; };
04496BD621625361001F1B23 /* ContentLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentLabel.swift; sourceTree = "<group>"; }; 04496BD621625361001F1B23 /* ContentLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentLabel.swift; sourceTree = "<group>"; };
0461A38F2163CBAE00C0A807 /* Cache.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Cache.framework; sourceTree = BUILT_PRODUCTS_DIR; };
04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabBarViewController.swift; sourceTree = "<group>"; }; 04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabBarViewController.swift; sourceTree = "<group>"; };
04DACE8D212CC7CC009840C4 /* AvatarCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarCache.swift; sourceTree = "<group>"; }; 04DACE8D212CC7CC009840C4 /* ImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = "<group>"; };
04ED00B021481ED800567C53 /* SteppedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SteppedProgressView.swift; sourceTree = "<group>"; }; 04ED00B021481ED800567C53 /* SteppedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SteppedProgressView.swift; sourceTree = "<group>"; };
D6028B9A2150811100F223B9 /* MastodonCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonCache.swift; sourceTree = "<group>"; }; D6028B9A2150811100F223B9 /* MastodonCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonCache.swift; sourceTree = "<group>"; };
D61099AB2144B0CC00432DC2 /* Pachyderm.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pachyderm.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D61099AB2144B0CC00432DC2 /* Pachyderm.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pachyderm.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@ -337,6 +341,7 @@
04496BCF216252E5001F1B23 /* TTTAttributedLabel.framework in Frameworks */, 04496BCF216252E5001F1B23 /* TTTAttributedLabel.framework in Frameworks */,
D61099C02144B0CC00432DC2 /* Pachyderm.framework in Frameworks */, D61099C02144B0CC00432DC2 /* Pachyderm.framework in Frameworks */,
D65A37F321472F300087646E /* SwiftSoup.framework in Frameworks */, D65A37F321472F300087646E /* SwiftSoup.framework in Frameworks */,
0461A3902163CBAE00C0A807 /* Cache.framework in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -667,6 +672,7 @@
D6D4DDC3212518A000E1C4BB = { D6D4DDC3212518A000E1C4BB = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
0461A38F2163CBAE00C0A807 /* Cache.framework */,
D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */, D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */,
D61099AC2144B0CC00432DC2 /* Pachyderm */, D61099AC2144B0CC00432DC2 /* Pachyderm */,
D61099B92144B0CC00432DC2 /* PachydermTests */, D61099B92144B0CC00432DC2 /* PachydermTests */,
@ -697,7 +703,7 @@
children = ( children = (
D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */, D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */,
D64D0AAC2128D88B005A6F37 /* LocalData.swift */, D64D0AAC2128D88B005A6F37 /* LocalData.swift */,
04DACE8D212CC7CC009840C4 /* AvatarCache.swift */, 04DACE8D212CC7CC009840C4 /* ImageCache.swift */,
D6028B9A2150811100F223B9 /* MastodonCache.swift */, D6028B9A2150811100F223B9 /* MastodonCache.swift */,
D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */, D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */,
D6757A7A2157E00100721E32 /* XCallbackURL */, D6757A7A2157E00100721E32 /* XCallbackURL */,
@ -1065,7 +1071,7 @@
D646C958213B367000269FB5 /* LargeImageShrinkAnimationController.swift in Sources */, D646C958213B367000269FB5 /* LargeImageShrinkAnimationController.swift in Sources */,
D646C956213B365700269FB5 /* LargeImageExpandAnimationController.swift in Sources */, D646C956213B365700269FB5 /* LargeImageExpandAnimationController.swift in Sources */,
D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */, D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */,
04DACE8E212CC7CC009840C4 /* AvatarCache.swift in Sources */, 04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */,
D6333B772138D94E00CE884A /* ComposeMediaView.swift in Sources */, D6333B772138D94E00CE884A /* ComposeMediaView.swift in Sources */,
04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */, 04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */,
D663626421360D2300C9CBA2 /* AvatarStyle.swift in Sources */, D663626421360D2300C9CBA2 /* AvatarStyle.swift in Sources */,

View File

@ -4,6 +4,9 @@
<FileRef <FileRef
location = "container:Tusker.xcodeproj"> location = "container:Tusker.xcodeproj">
</FileRef> </FileRef>
<FileRef
location = "group:Cache/Cache.xcodeproj">
</FileRef>
<FileRef <FileRef
location = "group:SwiftSoup/SwiftSoup.xcodeproj"> location = "group:SwiftSoup/SwiftSoup.xcodeproj">
</FileRef> </FileRef>

View File

@ -1,61 +0,0 @@
//
// ImageCache.swift
// Tusker
//
// Created by Shadowfactson 8/21/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import UIKit
class AvatarCache {
static let shared = AvatarCache()
let cache = NSCache<NSString, UIImage>()
var requests = [URL: URLSessionDataTask]()
var requestCallbacks = [URL: [(UIImage?) -> Void]]()
private init() {
}
func get(_ url: URL, completion: @escaping (UIImage?) -> Void) {
let key = url.absoluteString as NSString
if let image = cache.object(forKey: key) {
completion(image)
} else if var callbacks = requestCallbacks[url] {
callbacks.append(completion)
requestCallbacks[url] = callbacks
} else {
requestCallbacks[url] = [completion]
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard error == nil, let data = data, let image = UIImage(data: data) else {
let callbacks = self.requestCallbacks.removeValue(forKey: url)
callbacks?.forEach({ callback in
// todo: default avatar for failed requests
callback(nil)
})
return
}
let callbacks = self.requestCallbacks.removeValue(forKey: url)
callbacks?.forEach({ callback in
callback(image)
})
self.cache.setObject(image, forKey: key)
}
task.resume()
requests[url] = task
}
}
func cancel(_ url: URL) {
requests[url]?.cancel()
requests.removeValue(forKey: url)
requestCallbacks.removeValue(forKey: url)
}
}

85
Tusker/ImageCache.swift Normal file
View File

@ -0,0 +1,85 @@
//
// ImageCache.swift
// Tusker
//
// Created by Shadowfacts on 8/21/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import UIKit
import Cache
class ImageCache {
static let avatars = ImageCache(name: "Avatars")
let storage: Storage<UIImage>
var requests = [URL: Request]()
init(name: String, diskExpiry: Expiry = .seconds(60 * 60 * 24), memoryExpiry: Expiry = .seconds(60 * 60)) {
self.storage = try! Storage(
diskConfig: DiskConfig(name: name, expiry: diskExpiry),
memoryConfig: MemoryConfig(expiry: memoryExpiry),
transformer: TransformerFactory.forImage())
}
func get(_ url: URL, completion: ((UIImage?) -> Void)?) {
let key = url.absoluteString
if (try? storage.existsObject(forKey: key)) ?? false,
let image = try? storage.object(forKey: key) {
completion?(image)
} else {
if let completion = completion, let request = requests[url] {
request.callbacks.append(completion)
} else {
let request = Request(url: url, completion: completion)
requests[url] = request
request.run { (image) in
try? self.storage.setObject(image, forKey: key)
}
}
}
}
func cancel(_ url: URL) {
requests[url]?.cancel()
}
class Request {
let url: URL
var task: URLSessionDataTask?
var callbacks: [(UIImage?) -> Void]
init(url: URL, completion: ((UIImage?) -> Void)?) {
if let completion = completion {
self.callbacks = [completion]
} else {
self.callbacks = []
}
self.url = url
}
func run(cache: @escaping (UIImage) -> Void) {
task = URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) in
guard error == nil, let data = data, let image = UIImage(data: data) else {
self.complete(with: nil)
return
}
cache(image)
self.complete(with: image)
})
task!.resume()
}
func cancel() {
task?.cancel()
complete(with: nil)
}
func complete(with image: UIImage?) {
callbacks.forEach { $0(image) }
}
}
}

View File

@ -92,7 +92,7 @@ class ComposeViewController: UIViewController {
inReplyToAvatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: inReplyToAvatarImageView) inReplyToAvatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: inReplyToAvatarImageView)
inReplyToAvatarImageView.layer.masksToBounds = true inReplyToAvatarImageView.layer.masksToBounds = true
inReplyToAvatarImageView.image = nil inReplyToAvatarImageView.image = nil
AvatarCache.shared.get(inReplyTo.account.avatar) { image in ImageCache.avatars.get(inReplyTo.account.avatar) { (image) in
DispatchQueue.main.async { DispatchQueue.main.async {
self.inReplyToAvatarImageView.image = image self.inReplyToAvatarImageView.image = image
} }

View File

@ -75,7 +75,7 @@ class ActionNotificationTableViewCell: UITableViewCell, PreferencesAdaptive {
usernameLabel.text = "@\(status.account.acct)" usernameLabel.text = "@\(status.account.acct)"
opAvatarImageView.image = nil opAvatarImageView.image = nil
opAvatarURL = status.account.avatar opAvatarURL = status.account.avatar
AvatarCache.shared.get(status.account.avatar) { image in ImageCache.avatars.get(status.account.avatar) { (image) in
DispatchQueue.main.async { DispatchQueue.main.async {
self.opAvatarImageView.image = image self.opAvatarImageView.image = image
self.opAvatarURL = nil self.opAvatarURL = nil
@ -83,7 +83,7 @@ class ActionNotificationTableViewCell: UITableViewCell, PreferencesAdaptive {
} }
actionAvatarImageView.image = nil actionAvatarImageView.image = nil
actionAvatarURL = notification.account.avatar actionAvatarURL = notification.account.avatar
AvatarCache.shared.get(notification.account.avatar) { image in ImageCache.avatars.get(notification.account.avatar) { (image) in
DispatchQueue.main.async { DispatchQueue.main.async {
self.actionAvatarImageView.image = image self.actionAvatarImageView.image = image
self.actionAvatarURL = nil self.actionAvatarURL = nil
@ -158,10 +158,10 @@ class ActionNotificationTableViewCell: UITableViewCell, PreferencesAdaptive {
override func prepareForReuse() { override func prepareForReuse() {
if let url = opAvatarURL { if let url = opAvatarURL {
AvatarCache.shared.cancel(url) ImageCache.avatars.cancel(url)
} }
if let url = actionAvatarURL { if let url = actionAvatarURL {
AvatarCache.shared.cancel(url) ImageCache.avatars.cancel(url)
} }
updateTimestampWorkItem?.cancel() updateTimestampWorkItem?.cancel()
updateTimestampWorkItem = nil updateTimestampWorkItem = nil

View File

@ -49,7 +49,7 @@ class FollowNotificationTableViewCell: UITableViewCell, PreferencesAdaptive {
usernameLabel.text = "@\(account.acct)" usernameLabel.text = "@\(account.acct)"
avatarImageView.image = nil avatarImageView.image = nil
avatarURL = account.avatar avatarURL = account.avatar
AvatarCache.shared.get(account.avatar) { image in ImageCache.avatars.get(account.avatar) { (image) in
DispatchQueue.main.async { DispatchQueue.main.async {
self.avatarImageView.image = image self.avatarImageView.image = image
self.avatarURL = nil self.avatarURL = nil
@ -81,7 +81,7 @@ class FollowNotificationTableViewCell: UITableViewCell, PreferencesAdaptive {
override func prepareForReuse() { override func prepareForReuse() {
if let url = avatarURL { if let url = avatarURL {
AvatarCache.shared.cancel(url) ImageCache.avatars.cancel(url)
} }
updateTimestampWorkItem?.cancel() updateTimestampWorkItem?.cancel()
updateTimestampWorkItem = nil updateTimestampWorkItem = nil

View File

@ -59,7 +59,7 @@ class ProfileHeaderTableViewCell: UITableViewCell, PreferencesAdaptive {
avatarImageView.image = nil avatarImageView.image = nil
avatarURL = account.avatar avatarURL = account.avatar
AvatarCache.shared.get(account.avatar) { image in ImageCache.avatars.get(account.avatar) { (image) in
DispatchQueue.main.async { DispatchQueue.main.async {
self.avatarImageView.image = image self.avatarImageView.image = image
self.avatarURL = nil self.avatarURL = nil
@ -89,7 +89,7 @@ class ProfileHeaderTableViewCell: UITableViewCell, PreferencesAdaptive {
override func prepareForReuse() { override func prepareForReuse() {
if let url = avatarURL { if let url = avatarURL {
AvatarCache.shared.cancel(url) ImageCache.avatars.cancel(url)
} }
} }

View File

@ -77,7 +77,7 @@ class ConversationMainStatusTableViewCell: UITableViewCell, PreferencesAdaptive
usernameLabel.text = "@\(account.acct)" usernameLabel.text = "@\(account.acct)"
avatarImageView.image = nil avatarImageView.image = nil
avatarURL = account.avatar avatarURL = account.avatar
AvatarCache.shared.get(account.avatar) { image in ImageCache.avatars.get(account.avatar) { (image) in
DispatchQueue.main.async { DispatchQueue.main.async {
self.avatarImageView.image = image self.avatarImageView.image = image
self.avatarURL = nil self.avatarURL = nil
@ -151,7 +151,7 @@ class ConversationMainStatusTableViewCell: UITableViewCell, PreferencesAdaptive
override func prepareForReuse() { override func prepareForReuse() {
if let url = avatarURL { if let url = avatarURL {
AvatarCache.shared.cancel(url) ImageCache.avatars.cancel(url)
} }
updateTimestampWorkItem?.cancel() updateTimestampWorkItem?.cancel()
updateTimestampWorkItem = nil updateTimestampWorkItem = nil

View File

@ -92,7 +92,7 @@ class StatusTableViewCell: UITableViewCell, PreferencesAdaptive {
usernameLabel.text = "@\(account.acct)" usernameLabel.text = "@\(account.acct)"
avatarImageView.image = nil avatarImageView.image = nil
avatarURL = account.avatar avatarURL = account.avatar
AvatarCache.shared.get(account.avatar) { image in ImageCache.avatars.get(account.avatar) { (image) in
DispatchQueue.main.async { DispatchQueue.main.async {
self.avatarImageView.image = image self.avatarImageView.image = image
self.avatarURL = nil self.avatarURL = nil
@ -166,7 +166,7 @@ class StatusTableViewCell: UITableViewCell, PreferencesAdaptive {
override func prepareForReuse() { override func prepareForReuse() {
if let url = avatarURL { if let url = avatarURL {
AvatarCache.shared.cancel(url) ImageCache.avatars.cancel(url)
} }
updateTimestampWorkItem?.cancel() updateTimestampWorkItem?.cancel()
updateTimestampWorkItem = nil updateTimestampWorkItem = nil