Initial avatars implementation

This commit is contained in:
Shadowfacts 2018-08-21 19:23:27 -04:00
parent c42f5ee396
commit 4adc501073
Signed by: shadowfacts
GPG Key ID: 94A5AB95422746E5
4 changed files with 105 additions and 7 deletions

View File

@ -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 = "<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>"; };
D64D0AAC2128D88B005A6F37 /* LocalData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalData.swift; sourceTree = "<group>"; };
D64D0AAE2128D954005A6F37 /* Onboarding.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Onboarding.storyboard; sourceTree = "<group>"; };
D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = "<group>"; };
@ -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 = "<group>";
@ -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 */,

63
Tusker/AvatarCache.swift Normal file
View File

@ -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<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)!
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)
}
}

View File

@ -24,37 +24,48 @@
<rect key="frame" x="0.0" y="0.0" width="375" height="71.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Display name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ZFA-wR-9b4">
<rect key="frame" x="16" y="11" width="103" height="21"/>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="252" verticalHuggingPriority="251" text="Display name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ZFA-wR-9b4">
<rect key="frame" x="76" y="11" width="103" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="@Username" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Ffn-MC-jPW">
<rect key="frame" x="127" y="11" width="94" height="21"/>
<rect key="frame" x="187" y="11" width="172" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="0.33333333333333331" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" text="Content" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="xX6-Gq-MFH">
<rect key="frame" x="16" y="40" width="343" height="21"/>
<rect key="frame" x="76" y="40" width="283" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="7mj-Ts-blf">
<rect key="frame" x="16" y="11" width="50" height="50"/>
<constraints>
<constraint firstAttribute="width" secondItem="7mj-Ts-blf" secondAttribute="height" multiplier="1:1" id="9EB-YI-W3F"/>
<constraint firstAttribute="width" constant="50" id="Zon-62-0O6"/>
</constraints>
</imageView>
</subviews>
<constraints>
<constraint firstItem="xX6-Gq-MFH" firstAttribute="trailing" secondItem="JcO-MN-o4M" secondAttribute="trailingMargin" id="3uJ-2V-odU"/>
<constraint firstItem="Ffn-MC-jPW" firstAttribute="leading" secondItem="ZFA-wR-9b4" secondAttribute="trailing" constant="8" id="Az0-iw-JrZ"/>
<constraint firstItem="Ffn-MC-jPW" firstAttribute="top" secondItem="JcO-MN-o4M" secondAttribute="topMargin" id="I7N-O4-Q6U"/>
<constraint firstItem="ZFA-wR-9b4" firstAttribute="leading" secondItem="JcO-MN-o4M" secondAttribute="leadingMargin" id="OGD-sl-OQI"/>
<constraint firstItem="ZFA-wR-9b4" firstAttribute="leading" secondItem="7mj-Ts-blf" secondAttribute="trailing" constant="10" id="QJG-4Q-OfA"/>
<constraint firstItem="xX6-Gq-MFH" firstAttribute="bottom" secondItem="JcO-MN-o4M" secondAttribute="bottomMargin" id="Td8-7G-sJ7"/>
<constraint firstItem="ZFA-wR-9b4" firstAttribute="top" secondItem="JcO-MN-o4M" secondAttribute="topMargin" id="a0F-RV-jI0"/>
<constraint firstItem="xX6-Gq-MFH" firstAttribute="leading" secondItem="JcO-MN-o4M" secondAttribute="leadingMargin" id="oSD-4i-vYB"/>
<constraint firstAttribute="trailingMargin" secondItem="Ffn-MC-jPW" secondAttribute="trailing" id="eNm-X8-RAi"/>
<constraint firstItem="xX6-Gq-MFH" firstAttribute="leading" secondItem="7mj-Ts-blf" secondAttribute="trailing" constant="10" id="f2g-6V-rMH"/>
<constraint firstItem="7mj-Ts-blf" firstAttribute="top" secondItem="JcO-MN-o4M" secondAttribute="topMargin" id="qBF-aB-ko3"/>
<constraint firstItem="7mj-Ts-blf" firstAttribute="leading" secondItem="JcO-MN-o4M" secondAttribute="leadingMargin" id="tna-7N-d7r"/>
<constraint firstItem="xX6-Gq-MFH" firstAttribute="top" secondItem="ZFA-wR-9b4" secondAttribute="bottom" constant="8" id="ufm-zI-r2q"/>
</constraints>
</tableViewCellContentView>
<connections>
<outlet property="avatarImageView" destination="7mj-Ts-blf" id="t77-xO-FIM"/>
<outlet property="contentLabel" destination="xX6-Gq-MFH" id="d7X-0B-daj"/>
<outlet property="displayNameLabel" destination="ZFA-wR-9b4" id="4Bg-aE-tVd"/>
<outlet property="usernameLabel" destination="Ffn-MC-jPW" id="dfo-KY-c7S"/>

View File

@ -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)
}
}
}