diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 3a5f2187..c30ac977 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 04DACE8A212CA6B7009840C4 /* Timeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE89212CA6B7009840C4 /* Timeline.swift */; }; 04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */; }; + 04DACE8E212CC7CC009840C4 /* AvatarCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8D212CC7CC009840C4 /* AvatarCache.swift */; }; D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AAC2128D88B005A6F37 /* LocalData.swift */; }; D64D0AAF2128D954005A6F37 /* Onboarding.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D64D0AAE2128D954005A6F37 /* Onboarding.storyboard */; }; D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */; }; @@ -63,6 +64,7 @@ /* Begin PBXFileReference section */ 04DACE89212CA6B7009840C4 /* Timeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Timeline.swift; sourceTree = ""; }; 04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabBarViewController.swift; sourceTree = ""; }; + 04DACE8D212CC7CC009840C4 /* AvatarCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarCache.swift; sourceTree = ""; }; D64D0AAC2128D88B005A6F37 /* LocalData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalData.swift; sourceTree = ""; }; D64D0AAE2128D954005A6F37 /* Onboarding.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Onboarding.storyboard; sourceTree = ""; }; D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = ""; }; @@ -147,7 +149,9 @@ isa = PBXGroup; children = ( D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */, + 04DACE89212CA6B7009840C4 /* Timeline.swift */, D64D0AAC2128D88B005A6F37 /* LocalData.swift */, + 04DACE8D212CC7CC009840C4 /* AvatarCache.swift */, D6F953F121251A2F00CF0F2B /* Controllers */, D6F953E9212519B800CF0F2B /* View Controllers */, D6BED1722126661300F02DA0 /* Views */, @@ -155,7 +159,6 @@ D6D4DDD6212518A200E1C4BB /* Assets.xcassets */, D6D4DDD8212518A200E1C4BB /* LaunchScreen.storyboard */, D6D4DDDB212518A200E1C4BB /* Info.plist */, - 04DACE89212CA6B7009840C4 /* Timeline.swift */, ); path = Tusker; sourceTree = ""; @@ -343,6 +346,7 @@ 04DACE8A212CA6B7009840C4 /* Timeline.swift in Sources */, 04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */, D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */, + 04DACE8E212CC7CC009840C4 /* AvatarCache.swift in Sources */, D6BED174212667E900F02DA0 /* StatusTableViewCell.swift in Sources */, D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */, D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */, diff --git a/Tusker/AvatarCache.swift b/Tusker/AvatarCache.swift new file mode 100644 index 00000000..72859838 --- /dev/null +++ b/Tusker/AvatarCache.swift @@ -0,0 +1,63 @@ +// +// 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() + + 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)! + for callback in callbacks { + // todo: default avatar for failed requests + callback(nil) + } + return + } + + let callbacks = self.requestCallbacks.removeValue(forKey: url)! + for callback in callbacks { + 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) + } + +} diff --git a/Tusker/Storyboards/Timeline.storyboard b/Tusker/Storyboards/Timeline.storyboard index 37565668..6b89b3bb 100644 --- a/Tusker/Storyboards/Timeline.storyboard +++ b/Tusker/Storyboards/Timeline.storyboard @@ -24,37 +24,48 @@ - - + - + + + + + diff --git a/Tusker/Views/StatusTableViewCell.swift b/Tusker/Views/StatusTableViewCell.swift index a399e7ea..c7cf09e8 100644 --- a/Tusker/Views/StatusTableViewCell.swift +++ b/Tusker/Views/StatusTableViewCell.swift @@ -15,9 +15,12 @@ class StatusTableViewCell: UITableViewCell { @IBOutlet weak var displayNameLabel: UILabel! @IBOutlet weak var usernameLabel: UILabel! @IBOutlet weak var contentLabel: UILabel! + @IBOutlet weak var avatarImageView: UIImageView! var status: Status! + var avatarURL: URL? + var layoutManager: NSLayoutManager! var textContainer: NSTextContainer! var textStorage: NSTextStorage! @@ -35,6 +38,17 @@ class StatusTableViewCell: UITableViewCell { } displayNameLabel.text = account.displayName usernameLabel.text = "@\(account.acct)" + avatarImageView.layer.cornerRadius = 5 + avatarImageView.layer.masksToBounds = true + avatarImageView.image = nil + if let url = URL(string: account.avatar) { + AvatarCache.shared.get(url) { image in + DispatchQueue.main.async { + self.avatarImageView.image = image + } + } + } + let doc = try! SwiftSoup.parse(status.content) let body = doc.body()! @@ -114,5 +128,11 @@ class StatusTableViewCell: UITableViewCell { print("Open URL: \(url)") } } + + override func prepareForReuse() { + if let url = avatarURL { + AvatarCache.shared.cancel(url) + } + } }