Merge branch 'image-caching'
This commit is contained in:
commit
b796f288c8
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -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
1
Cache
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 8c42c575cf28b2ff0e780c9728721e9a8891c92e
|
@ -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 */,
|
||||
|
3
Tusker.xcworkspace/contents.xcworkspacedata
generated
3
Tusker.xcworkspace/contents.xcworkspacedata
generated
@ -4,6 +4,9 @@
|
||||
<FileRef
|
||||
location = "container:Tusker.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Cache/Cache.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:SwiftSoup/SwiftSoup.xcodeproj">
|
||||
</FileRef>
|
||||
|
@ -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
87
Tusker/ImageCache.swift
Normal 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) }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user