Merge branch 'image-caching'

This commit is contained in:
Shadowfacts 2018-10-03 08:53:17 -04:00
commit b796f288c8
Signed by: shadowfacts
GPG Key ID: 94A5AB95422746E5
13 changed files with 126 additions and 97 deletions

3
.gitmodules vendored
View File

@ -1,3 +1,6 @@
[submodule "SwiftSoup"]
path = SwiftSoup
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, ); }; };
04496BD52162530A001F1B23 /* TTTAttributedLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = 04496BD42162530A001F1B23 /* TTTAttributedLabel.m */; };
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 */; };
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 */; };
D6028B9B2150811100F223B9 /* MastodonCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6028B9A2150811100F223B9 /* MastodonCache.swift */; };
D61099B42144B0CC00432DC2 /* Pachyderm.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D61099AB2144B0CC00432DC2 /* Pachyderm.framework */; };
@ -181,6 +183,7 @@
04496BD0216252E5001F1B23 /* TTTAttributedLabel.framework in Embed Frameworks */,
D61099C12144B0CC00432DC2 /* Pachyderm.framework in Embed Frameworks */,
D6BED170212663DA00F02DA0 /* SwiftSoup.framework in Embed Frameworks */,
0461A3912163CBAE00C0A807 /* Cache.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
@ -193,8 +196,9 @@
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>"; };
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>"; };
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>"; };
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; };
@ -343,6 +347,7 @@
04496BCF216252E5001F1B23 /* TTTAttributedLabel.framework in Frameworks */,
D61099C02144B0CC00432DC2 /* Pachyderm.framework in Frameworks */,
D65A37F321472F300087646E /* SwiftSoup.framework in Frameworks */,
0461A3902163CBAE00C0A807 /* Cache.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -684,6 +689,7 @@
D6D4DDC3212518A000E1C4BB = {
isa = PBXGroup;
children = (
0461A38F2163CBAE00C0A807 /* Cache.framework */,
D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */,
D61099AC2144B0CC00432DC2 /* Pachyderm */,
D61099B92144B0CC00432DC2 /* PachydermTests */,
@ -714,7 +720,7 @@
children = (
D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */,
D64D0AAC2128D88B005A6F37 /* LocalData.swift */,
04DACE8D212CC7CC009840C4 /* AvatarCache.swift */,
04DACE8D212CC7CC009840C4 /* ImageCache.swift */,
D6028B9A2150811100F223B9 /* MastodonCache.swift */,
D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */,
D6757A7A2157E00100721E32 /* XCallbackURL */,
@ -1082,7 +1088,7 @@
D646C958213B367000269FB5 /* LargeImageShrinkAnimationController.swift in Sources */,
D646C956213B365700269FB5 /* LargeImageExpandAnimationController.swift in Sources */,
D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */,
04DACE8E212CC7CC009840C4 /* AvatarCache.swift in Sources */,
04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */,
D6333B772138D94E00CE884A /* ComposeMediaView.swift in Sources */,
04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */,
D663626421360D2300C9CBA2 /* AvatarStyle.swift in Sources */,

View File

@ -4,6 +4,9 @@
<FileRef
location = "container:Tusker.xcodeproj">
</FileRef>
<FileRef
location = "group:Cache/Cache.xcodeproj">
</FileRef>
<FileRef
location = "group:SwiftSoup/SwiftSoup.xcodeproj">
</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)
}
}

87
Tusker/ImageCache.swift Normal file
View File

@ -0,0 +1,87 @@
//
// 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")
static let headers = ImageCache(name: "Headers")
static let attachments = ImageCache(name: "Attachments", diskExpiry: .seconds(60 * 60), memoryExpiry: .seconds(60))
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.masksToBounds = true
inReplyToAvatarImageView.image = nil
AvatarCache.shared.get(inReplyTo.account.avatar) { image in
ImageCache.avatars.get(inReplyTo.account.avatar) { (image) in
DispatchQueue.main.async {
self.inReplyToAvatarImageView.image = image
}

View File

@ -19,8 +19,6 @@ class AttachmentView: UIImageView {
var attachment: Attachment!
var task: URLSessionDataTask?
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
@ -45,13 +43,11 @@ class AttachmentView: UIImageView {
}
func loadImage() {
task = URLSession.shared.dataTask(with: attachment.url) { data, response, error in
guard error == nil, let data = data, let image = UIImage(data: data) else { return }
ImageCache.attachments.get(attachment.url) { (image) in
DispatchQueue.main.async {
self.image = image
}
}
task!.resume()
}
@objc func imagePressed() {

View File

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

View File

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

View File

@ -28,8 +28,7 @@ class ProfileHeaderTableViewCell: UITableViewCell, PreferencesAdaptive {
var accountID: String!
var avatarURL: URL?
var headerImageDownloadTask: URLSessionDataTask?
var headerURL: URL?
override func awakeFromNib() {
avatarContainerView.layer.masksToBounds = true
@ -59,20 +58,18 @@ class ProfileHeaderTableViewCell: UITableViewCell, PreferencesAdaptive {
avatarImageView.image = nil
avatarURL = account.avatar
AvatarCache.shared.get(account.avatar) { image in
ImageCache.avatars.get(account.avatar) { (image) in
DispatchQueue.main.async {
self.avatarImageView.image = image
self.avatarURL = nil
}
}
headerImageDownloadTask = URLSession.shared.dataTask(with: account.header) { data, response, error in
guard error == nil, let data = data, let image = UIImage(data: data) else { return }
ImageCache.headers.get(account.header) { (image) in
DispatchQueue.main.async {
self.headerImageView.image = image
self.headerImageDownloadTask = nil
self.headerURL = nil
}
}
headerImageDownloadTask!.resume()
noteLabel.setTextFromHtml(account.note)
@ -92,7 +89,10 @@ class ProfileHeaderTableViewCell: UITableViewCell, PreferencesAdaptive {
override func prepareForReuse() {
if let url = avatarURL {
AvatarCache.shared.cancel(url)
ImageCache.avatars.cancel(url)
}
if let url = headerURL {
ImageCache.headers.cancel(url)
}
}

View File

@ -77,7 +77,7 @@ class ConversationMainStatusTableViewCell: UITableViewCell, PreferencesAdaptive
usernameLabel.text = "@\(account.acct)"
avatarImageView.image = nil
avatarURL = account.avatar
AvatarCache.shared.get(account.avatar) { image in
ImageCache.avatars.get(account.avatar) { (image) in
DispatchQueue.main.async {
self.avatarImageView.image = image
self.avatarURL = nil
@ -85,7 +85,6 @@ class ConversationMainStatusTableViewCell: UITableViewCell, PreferencesAdaptive
}
updateTimestamp()
attachmentsView.subviews.forEach { $0.removeFromSuperview() }
let attachments = status.attachments.filter({ $0.kind == .image })
if attachments.count > 0 {
attachmentsView.isHidden = false
@ -151,13 +150,11 @@ class ConversationMainStatusTableViewCell: UITableViewCell, PreferencesAdaptive
override func prepareForReuse() {
if let url = avatarURL {
AvatarCache.shared.cancel(url)
ImageCache.avatars.cancel(url)
}
updateTimestampWorkItem?.cancel()
updateTimestampWorkItem = nil
attachmentsView.subviews.forEach { view in
(view as? AttachmentView)?.task?.cancel()
}
attachmentsView.subviews.forEach { $0.removeFromSuperview() }
}
@objc func accountPressed() {

View File

@ -92,7 +92,7 @@ class StatusTableViewCell: UITableViewCell, PreferencesAdaptive {
usernameLabel.text = "@\(account.acct)"
avatarImageView.image = nil
avatarURL = account.avatar
AvatarCache.shared.get(account.avatar) { image in
ImageCache.avatars.get(account.avatar) { (image) in
DispatchQueue.main.async {
self.avatarImageView.image = image
self.avatarURL = nil
@ -166,14 +166,11 @@ class StatusTableViewCell: UITableViewCell, PreferencesAdaptive {
override func prepareForReuse() {
if let url = avatarURL {
AvatarCache.shared.cancel(url)
ImageCache.avatars.cancel(url)
}
updateTimestampWorkItem?.cancel()
updateTimestampWorkItem = nil
attachmentsView.subviews.forEach { view in
(view as? AttachmentView)?.task?.cancel()
view.removeFromSuperview()
}
attachmentsView.subviews.forEach { $0.removeFromSuperview() }
}
override func setSelected(_ selected: Bool, animated: Bool) {