Compare commits

...

6 Commits

Author SHA1 Message Date
Shadowfacts 3aa5aa1bc0
Fix weird crashes when switching accounts 2020-01-19 23:16:36 -05:00
Shadowfacts ee252c02e9
Fix retain cycle in timeline cell cache observers
The use an unowned reference to self because when the cell is deinit'd,
the Combine observers will be cancelled.
2020-01-19 23:14:51 -05:00
Shadowfacts 2f630f2f8f
Fix retain cycle between MastodonController/MastodonCache
The cache should only store a weak reference to the controller, so that
when the controller is deinit'd the cache is as well.
2020-01-19 23:14:13 -05:00
Shadowfacts 8eb6f6f573
Fix retain cycle in timestamp updating code
The timestamp update work item shouldn't retain a reference to the cell.
It can be unowned because when the cell is deinit'd, the work item will
be cancelled.
2020-01-19 23:10:52 -05:00
Shadowfacts 32e89f2c16
Fix retain cycles with TuskerNavigationDelegate
TuskerNavigationDelegate is now class-bound and only weak references to
it are stored.
2020-01-19 23:02:07 -05:00
Shadowfacts c45dd99088
Clean up account switching code 2020-01-19 11:52:06 -05:00
27 changed files with 140 additions and 74 deletions

View File

@ -21,6 +21,7 @@ class LocalData: ObservableObject {
if ProcessInfo.processInfo.environment.keys.contains("UI_TESTING_LOGIN") { if ProcessInfo.processInfo.environment.keys.contains("UI_TESTING_LOGIN") {
accounts = [ accounts = [
UserAccountInfo( UserAccountInfo(
id: UUID().uuidString,
instanceURL: URL(string: "http://localhost:8080")!, instanceURL: URL(string: "http://localhost:8080")!,
clientID: "client_id", clientID: "client_id",
clientSecret: "client_secret", clientSecret: "client_secret",
@ -38,15 +39,16 @@ class LocalData: ObservableObject {
get { get {
if let array = defaults.array(forKey: accountsKey) as? [[String: String]] { if let array = defaults.array(forKey: accountsKey) as? [[String: String]] {
return array.compactMap { (info) in return array.compactMap { (info) in
guard let instanceURL = info["instanceURL"], guard let id = info["id"],
let instanceURL = info["instanceURL"],
let url = URL(string: instanceURL), let url = URL(string: instanceURL),
let id = info["clientID"], let clientId = info["clientID"],
let secret = info["clientSecret"], let secret = info["clientSecret"],
let username = info["username"], let username = info["username"],
let accessToken = info["accessToken"] else { let accessToken = info["accessToken"] else {
return nil return nil
} }
return UserAccountInfo(instanceURL: url, clientID: id, clientSecret: secret, username: username, accessToken: accessToken) return UserAccountInfo(id: id, instanceURL: url, clientID: clientId, clientSecret: secret, username: username, accessToken: accessToken)
} }
} else { } else {
return [] return []
@ -56,6 +58,7 @@ class LocalData: ObservableObject {
objectWillChange.send() objectWillChange.send()
let array = newValue.map { (info) in let array = newValue.map { (info) in
return [ return [
"id": info.id,
"instanceURL": info.instanceURL.absoluteString, "instanceURL": info.instanceURL.absoluteString,
"clientID": info.clientID, "clientID": info.clientID,
"clientSecret": info.clientSecret, "clientSecret": info.clientSecret,
@ -68,7 +71,7 @@ class LocalData: ObservableObject {
} }
private let mostRecentAccountKey = "mostRecentAccount" private let mostRecentAccountKey = "mostRecentAccount"
var mostRecentAccount: String? { private var mostRecentAccount: String? {
get { get {
return defaults.string(forKey: mostRecentAccountKey) return defaults.string(forKey: mostRecentAccountKey)
} }
@ -82,41 +85,60 @@ class LocalData: ObservableObject {
return !accounts.isEmpty return !accounts.isEmpty
} }
func addAccount(instanceURL url: URL, clientID id: String, clientSecret secret: String, username: String, accessToken: String) -> UserAccountInfo { func addAccount(instanceURL url: URL, clientID: String, clientSecret secret: String, username: String, accessToken: String) -> UserAccountInfo {
var accounts = self.accounts var accounts = self.accounts
if let index = accounts.firstIndex(where: { $0.instanceURL == url && $0.username == username }) { if let index = accounts.firstIndex(where: { $0.instanceURL == url && $0.username == username }) {
accounts.remove(at: index) accounts.remove(at: index)
} }
let info = UserAccountInfo(instanceURL: url, clientID: id, clientSecret: secret, username: username, accessToken: accessToken) let id = UUID().uuidString
let info = UserAccountInfo(id: id, instanceURL: url, clientID: clientID, clientSecret: secret, username: username, accessToken: accessToken)
accounts.append(info) accounts.append(info)
self.accounts = accounts self.accounts = accounts
return info return info
} }
func removeAccount(_ info: UserAccountInfo) { func removeAccount(_ info: UserAccountInfo) {
accounts.removeAll(where: { $0.id == info.id })
} }
func getMostRecentAccount() -> UserAccountInfo? { func getMostRecentAccount() -> UserAccountInfo? {
if let accessToken = mostRecentAccount { guard onboardingComplete else { return nil }
return accounts.first { $0.accessToken == accessToken } let mostRecent: UserAccountInfo?
if let id = mostRecentAccount {
mostRecent = accounts.first { $0.id == id }
} else { } else {
return nil mostRecent = nil
} }
return mostRecent ?? accounts.first!
}
func setMostRecentAccount(_ account: UserAccountInfo?) {
mostRecentAccount = account?.id
} }
} }
extension LocalData { extension LocalData {
struct UserAccountInfo: Equatable, Hashable { struct UserAccountInfo: Equatable, Hashable {
let id: String
let instanceURL: URL let instanceURL: URL
let clientID: String let clientID: String
let clientSecret: String let clientSecret: String
let username: String let username: String
let accessToken: String let accessToken: String
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
static func ==(lhs: UserAccountInfo, rhs: UserAccountInfo) -> Bool {
return lhs.id == rhs.id
}
} }
} }
extension Notification.Name { extension Notification.Name {
static let userLoggedOut = Notification.Name("userLoggedOut") static let userLoggedOut = Notification.Name("Tusker.userLoggedOut")
static let addAccount = Notification.Name("Tusker.addAccount")
static let activateAccount = Notification.Name("Tusker.activateAccount")
} }

View File

@ -20,7 +20,7 @@ class MastodonCache {
let statusSubject = PassthroughSubject<Status, Never>() let statusSubject = PassthroughSubject<Status, Never>()
let accountSubject = PassthroughSubject<Account, Never>() let accountSubject = PassthroughSubject<Account, Never>()
let mastodonController: MastodonController weak var mastodonController: MastodonController?
init(mastodonController: MastodonController) { init(mastodonController: MastodonController) {
self.mastodonController = mastodonController self.mastodonController = mastodonController
@ -43,6 +43,9 @@ class MastodonCache {
} }
func status(for id: String, completion: @escaping (Status?) -> Void) { func status(for id: String, completion: @escaping (Status?) -> Void) {
guard let mastodonController = mastodonController else {
fatalError("The MastodonController for this cache has been deinitialized, so this cache should no longer exist. Are you storing a strong reference to it?")
}
let request = Client.getStatus(id: id) let request = Client.getStatus(id: id)
mastodonController.run(request) { response in mastodonController.run(request) { response in
guard case let .success(status, _) = response else { guard case let .success(status, _) = response else {
@ -73,6 +76,9 @@ class MastodonCache {
} }
func account(for id: String, completion: @escaping (Account?) -> Void) { func account(for id: String, completion: @escaping (Account?) -> Void) {
guard let mastodonController = mastodonController else {
fatalError("The MastodonController for this cache has been deinitialized, so this cache should no longer exist. Are you storing a strong reference to it?")
}
let request = Client.getAccount(id: id) let request = Client.getAccount(id: id)
mastodonController.run(request) { response in mastodonController.run(request) { response in
guard case let .success(account, _) = response else { guard case let .success(account, _) = response else {
@ -102,6 +108,9 @@ class MastodonCache {
} }
func relationship(for id: String, completion: @escaping (Relationship?) -> Void) { func relationship(for id: String, completion: @escaping (Relationship?) -> Void) {
guard let mastodonController = mastodonController else {
fatalError("The MastodonController for this cache has been deinitialized, so this cache should no longer exist. Are you storing a strong reference to it?")
}
let request = Client.getRelationships(accounts: [id]) let request = Client.getRelationships(accounts: [id])
mastodonController.run(request) { response in mastodonController.run(request) { response in
guard case let .success(relationships, _) = response, guard case let .success(relationships, _) = response,

View File

@ -34,7 +34,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
window!.makeKeyAndVisible() window!.makeKeyAndVisible()
NotificationCenter.default.addObserver(self, selector: #selector(onUserLoggedOut), name: .userLoggedOut, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(themePrefChanged), name: .themePreferenceChanged, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(themePrefChanged), name: .themePreferenceChanged, object: nil)
themePrefChanged() themePrefChanged()
@ -110,11 +109,20 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
} }
func activateAccount(_ account: LocalData.UserAccountInfo) { func activateAccount(_ account: LocalData.UserAccountInfo) {
LocalData.shared.mostRecentAccount = account.accessToken LocalData.shared.setMostRecentAccount(account)
window!.windowScene!.session.mastodonController = MastodonController.getForAccount(account) window!.windowScene!.session.mastodonController = MastodonController.getForAccount(account)
showAppUI() showAppUI()
} }
func logoutCurrent() {
LocalData.shared.removeAccount(LocalData.shared.getMostRecentAccount()!)
if LocalData.shared.onboardingComplete {
activateAccount(LocalData.shared.accounts.first!)
} else {
showOnboardingUI()
}
}
func showAppUI() { func showAppUI() {
let mastodonController = window!.windowScene!.session.mastodonController! let mastodonController = window!.windowScene!.session.mastodonController!
mastodonController.getOwnAccount() mastodonController.getOwnAccount()
@ -130,10 +138,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
window!.rootViewController = onboarding window!.rootViewController = onboarding
} }
@objc func onUserLoggedOut() {
showOnboardingUI()
}
@objc func themePrefChanged() { @objc func themePrefChanged() {
window?.overrideUserInterfaceStyle = Preferences.shared.theme window?.overrideUserInterfaceStyle = Preferences.shared.theme
} }

View File

@ -8,7 +8,7 @@
import UIKit import UIKit
protocol DraftsTableViewControllerDelegate { protocol DraftsTableViewControllerDelegate: class {
func draftSelectionCanceled() func draftSelectionCanceled()
func shouldSelectDraft(_ draft: DraftsManager.Draft, completion: @escaping (Bool) -> Void) func shouldSelectDraft(_ draft: DraftsManager.Draft, completion: @escaping (Bool) -> Void)
func draftSelected(_ draft: DraftsManager.Draft) func draftSelected(_ draft: DraftsManager.Draft)
@ -17,7 +17,7 @@ protocol DraftsTableViewControllerDelegate {
class DraftsTableViewController: UITableViewController { class DraftsTableViewController: UITableViewController {
var delegate: DraftsTableViewControllerDelegate? weak var delegate: DraftsTableViewControllerDelegate?
init() { init() {
super.init(nibName: "DraftsTableViewController", bundle: nil) super.init(nibName: "DraftsTableViewController", bundle: nil)

View File

@ -10,7 +10,7 @@ import UIKit
class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate { class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
let mastodonController: MastodonController weak var mastodonController: MastodonController!
override var supportedInterfaceOrientations: UIInterfaceOrientationMask { override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
if UIDevice.current.userInterfaceIdiom == .phone { if UIDevice.current.userInterfaceIdiom == .phone {

View File

@ -14,7 +14,7 @@ class NotificationsPageViewController: SegmentedPageViewController {
private let notificationsTitle = NSLocalizedString("Notifications", comment: "notifications tab title") private let notificationsTitle = NSLocalizedString("Notifications", comment: "notifications tab title")
private let mentionsTitle = NSLocalizedString("Mentions", comment: "mentions tab title") private let mentionsTitle = NSLocalizedString("Mentions", comment: "mentions tab title")
let mastodonController: MastodonController weak var mastodonController: MastodonController!
init(mastodonController: MastodonController) { init(mastodonController: MastodonController) {
self.mastodonController = mastodonController self.mastodonController = mastodonController

View File

@ -10,7 +10,7 @@ import UIKit
import Combine import Combine
import Pachyderm import Pachyderm
protocol InstanceSelectorTableViewControllerDelegate { protocol InstanceSelectorTableViewControllerDelegate: class {
func didSelectInstance(url: URL) func didSelectInstance(url: URL)
} }
@ -18,7 +18,7 @@ fileprivate let instanceCell = "instanceCell"
class InstanceSelectorTableViewController: UITableViewController { class InstanceSelectorTableViewController: UITableViewController {
var delegate: InstanceSelectorTableViewControllerDelegate? weak var delegate: InstanceSelectorTableViewControllerDelegate?
var dataSource: DataSource! var dataSource: DataSource!
var searchController: UISearchController! var searchController: UISearchController!
@ -55,7 +55,7 @@ class InstanceSelectorTableViewController: UITableViewController {
tableView.rowHeight = UITableView.automaticDimension tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 120 tableView.estimatedRowHeight = 120
dataSource = DataSource(tableView: tableView, cellProvider: { (tableView, indexPath, item) -> UITableViewCell? in dataSource = DataSource(tableView: tableView, cellProvider: { [weak self] (tableView, indexPath, item) -> UITableViewCell? in
switch item { switch item {
case let .selected(instance): case let .selected(instance):
let cell = tableView.dequeueReusableCell(withIdentifier: instanceCell, for: indexPath) as! InstanceTableViewCell let cell = tableView.dequeueReusableCell(withIdentifier: instanceCell, for: indexPath) as! InstanceTableViewCell

View File

@ -69,9 +69,9 @@ extension OnboardingViewController: InstanceSelectorTableViewControllerDelegate
mastodonController.authorize(authorizationCode: authCode) { (accessToken) in mastodonController.authorize(authorizationCode: authCode) { (accessToken) in
mastodonController.getOwnAccount { (account) in mastodonController.getOwnAccount { (account) in
DispatchQueue.main.async {
let accountInfo = LocalData.shared.addAccount(instanceURL: instanceURL, clientID: clientID, clientSecret: clientSecret, username: account.username, accessToken: accessToken) let accountInfo = LocalData.shared.addAccount(instanceURL: instanceURL, clientID: clientID, clientSecret: clientSecret, username: account.username, accessToken: accessToken)
DispatchQueue.main.async {
self.onboardingDelegate?.didFinishOnboarding(account: accountInfo) self.onboardingDelegate?.didFinishOnboarding(account: accountInfo)
} }
} }

View File

@ -11,6 +11,8 @@ import SwiftUI
class PreferencesNavigationController: UINavigationController { class PreferencesNavigationController: UINavigationController {
private var isSwitchingAccounts = false
init(mastodonController: MastodonController) { init(mastodonController: MastodonController) {
let view = PreferencesView() let view = PreferencesView()
let hostingController = UIHostingController(rootView: view) let hostingController = UIHostingController(rootView: view)
@ -27,14 +29,17 @@ class PreferencesNavigationController: UINavigationController {
NotificationCenter.default.addObserver(self, selector: #selector(showAddAccount), name: .addAccount, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(showAddAccount), name: .addAccount, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(activateAccount(_:)), name: .activateAccount, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(activateAccount(_:)), name: .activateAccount, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(userLoggedOut), name: .userLoggedOut, object: nil)
} }
override func viewWillDisappear(_ animated: Bool) { override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated) super.viewWillDisappear(animated)
if !isSwitchingAccounts {
// workaround for onDisappear not being called when a modally presented UIHostingController is dismissed // workaround for onDisappear not being called when a modally presented UIHostingController is dismissed
NotificationCenter.default.post(name: .preferencesChanged, object: nil) NotificationCenter.default.post(name: .preferencesChanged, object: nil)
} }
}
@objc func donePressed() { @objc func donePressed() {
dismiss(animated: true) dismiss(animated: true)
@ -48,26 +53,37 @@ class PreferencesNavigationController: UINavigationController {
} }
@objc func cancelAddAccount() { @objc func cancelAddAccount() {
dismiss(animated: true) dismiss(animated: true) // dismisses instance selector
} }
@objc func activateAccount(_ notification: Notification) { @objc func activateAccount(_ notification: Notification) {
let account = notification.userInfo!["account"] as! LocalData.UserAccountInfo let account = notification.userInfo!["account"] as! LocalData.UserAccountInfo
let sceneDelegate = self.view.window!.windowScene!.delegate as! SceneDelegate let sceneDelegate = self.view.window!.windowScene!.delegate as! SceneDelegate
dismiss(animated: true) { isSwitchingAccounts = true
dismiss(animated: true) { // dismiss preferences
sceneDelegate.activateAccount(account) sceneDelegate.activateAccount(account)
} }
} }
} @objc func userLoggedOut() {
let sceneDelegate = self.view.window!.windowScene!.delegate as! SceneDelegate
isSwitchingAccounts = true
dismiss(animated: true) { // dismiss preferences
sceneDelegate.logoutCurrent()
}
}
extension Notification.Name {
static let addAccount = Notification.Name("Tusker.addAccount")
static let activateAccount = Notification.Name("Tusker.activateAccount")
} }
extension PreferencesNavigationController: OnboardingViewControllerDelegate { extension PreferencesNavigationController: OnboardingViewControllerDelegate {
func didFinishOnboarding(account: LocalData.UserAccountInfo) { func didFinishOnboarding(account: LocalData.UserAccountInfo) {
LocalData.shared.mostRecentAccount = account.accessToken DispatchQueue.main.async {
let sceneDelegate = self.view.window!.windowScene!.delegate as! SceneDelegate
self.dismiss(animated: true) { // dismiss instance selector
self.dismiss(animated: true) { // dismiss preferences
sceneDelegate.activateAccount(account)
}
}
}
} }
} }

View File

@ -24,7 +24,7 @@ struct PreferencesView: View {
Text(account.username) Text(account.username)
.foregroundColor(.primary) .foregroundColor(.primary)
Spacer() Spacer()
if account.accessToken == self.localData.mostRecentAccount { if account == self.localData.getMostRecentAccount() {
Image(systemName: "checkmark") Image(systemName: "checkmark")
.renderingMode(.template) .renderingMode(.template)
.foregroundColor(.secondary) .foregroundColor(.secondary)
@ -37,7 +37,7 @@ struct PreferencesView: View {
}) { }) {
Text("Add Account...") Text("Add Account...")
} }
if localData.mostRecentAccount != nil { if localData.getMostRecentAccount() != nil {
Button(action: { Button(action: {
self.showingLogoutConfirmation = true self.showingLogoutConfirmation = true
}) { }) {
@ -73,8 +73,6 @@ struct PreferencesView: View {
} }
func logoutPressed() { func logoutPressed() {
// LocalData.shared.removeAccount(currentAccount)
localData.removeAccount(localData.getMostRecentAccount()!)
NotificationCenter.default.post(name: .userLoggedOut, object: nil) NotificationCenter.default.post(name: .userLoggedOut, object: nil)
} }
} }

View File

@ -12,7 +12,7 @@ import SafariServices
class ProfileTableViewController: EnhancedTableViewController { class ProfileTableViewController: EnhancedTableViewController {
let mastodonController: MastodonController weak var mastodonController: MastodonController!
var accountID: String! { var accountID: String! {
didSet { didSet {

View File

@ -8,14 +8,14 @@
import UIKit import UIKit
protocol InstanceTimelineViewControllerDelegate { protocol InstanceTimelineViewControllerDelegate: class {
func didSaveInstance(url: URL) func didSaveInstance(url: URL)
func didUnsaveInstance(url: URL) func didUnsaveInstance(url: URL)
} }
class InstanceTimelineViewController: TimelineTableViewController { class InstanceTimelineViewController: TimelineTableViewController {
var delegate: InstanceTimelineViewControllerDelegate? weak var delegate: InstanceTimelineViewControllerDelegate?
let instanceURL: URL let instanceURL: URL

View File

@ -12,7 +12,7 @@ import Pachyderm
class TimelineTableViewController: EnhancedTableViewController { class TimelineTableViewController: EnhancedTableViewController {
var timeline: Timeline! var timeline: Timeline!
let mastodonController: MastodonController weak var mastodonController: MastodonController!
var timelineSegments: [[(id: String, state: StatusState)]] = [] { var timelineSegments: [[(id: String, state: StatusState)]] = [] {
didSet { didSet {

View File

@ -14,7 +14,7 @@ class TimelinesPageViewController: SegmentedPageViewController {
private let federatedTitle = NSLocalizedString("Federated", comment: "federated timeline tab title") private let federatedTitle = NSLocalizedString("Federated", comment: "federated timeline tab title")
private let localTitle = NSLocalizedString("Local", comment: "local timeline tab title") private let localTitle = NSLocalizedString("Local", comment: "local timeline tab title")
let mastodonController: MastodonController weak var mastodonController: MastodonController!
init(mastodonController: MastodonController) { init(mastodonController: MastodonController) {
self.mastodonController = mastodonController self.mastodonController = mastodonController

View File

@ -10,7 +10,7 @@ import UIKit
import SafariServices import SafariServices
import Pachyderm import Pachyderm
protocol TuskerNavigationDelegate { protocol TuskerNavigationDelegate: class {
var apiController: MastodonController { get } var apiController: MastodonController { get }

View File

@ -10,7 +10,7 @@ import UIKit
class AccountTableViewCell: UITableViewCell { class AccountTableViewCell: UITableViewCell {
var delegate: TuskerNavigationDelegate? weak var delegate: TuskerNavigationDelegate?
var mastodonController: MastodonController! { delegate?.apiController } var mastodonController: MastodonController! { delegate?.apiController }
@IBOutlet weak var avatarImageView: UIImageView! @IBOutlet weak var avatarImageView: UIImageView!

View File

@ -11,13 +11,13 @@ import Pachyderm
import Gifu import Gifu
import AVFoundation import AVFoundation
protocol AttachmentViewDelegate { protocol AttachmentViewDelegate: class {
func showAttachmentsGallery(startingAt index: Int) func showAttachmentsGallery(startingAt index: Int)
} }
class AttachmentView: UIImageView, GIFAnimatable { class AttachmentView: UIImageView, GIFAnimatable {
var delegate: AttachmentViewDelegate? weak var delegate: AttachmentViewDelegate?
var playImageView: UIImageView! var playImageView: UIImageView!
@ -71,8 +71,8 @@ class AttachmentView: UIImageView, GIFAnimatable {
} }
func loadImage() { func loadImage() {
ImageCache.attachments.get(attachment.url) { (data) in ImageCache.attachments.get(attachment.url) { [weak self] (data) in
guard let data = data else { return } guard let self = self, let data = data else { return }
DispatchQueue.main.async { DispatchQueue.main.async {
if self.attachment.url.pathExtension == "gif" { if self.attachment.url.pathExtension == "gif" {
self.animate(withGIFData: data) self.animate(withGIFData: data)

View File

@ -11,7 +11,7 @@ import Pachyderm
class AttachmentsContainerView: UIView { class AttachmentsContainerView: UIView {
var delegate: AttachmentViewDelegate? weak var delegate: AttachmentViewDelegate?
var statusID: String! var statusID: String!
var attachments: [Attachment]! var attachments: [Attachment]!

View File

@ -10,14 +10,14 @@ import UIKit
import Photos import Photos
import AVFoundation import AVFoundation
protocol ComposeMediaViewDelegate { protocol ComposeMediaViewDelegate: class {
func didRemoveMedia(_ mediaView: ComposeMediaView) func didRemoveMedia(_ mediaView: ComposeMediaView)
func descriptionTextViewDidChange(_ mediaView: ComposeMediaView) func descriptionTextViewDidChange(_ mediaView: ComposeMediaView)
} }
class ComposeMediaView: UIView { class ComposeMediaView: UIView {
var delegate: ComposeMediaViewDelegate? weak var delegate: ComposeMediaViewDelegate?
@IBOutlet weak var imageView: UIImageView! @IBOutlet weak var imageView: UIImageView!
@IBOutlet weak var descriptionTextView: UITextView! @IBOutlet weak var descriptionTextView: UITextView!

View File

@ -15,8 +15,7 @@ private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options:
class ContentTextView: LinkTextView { class ContentTextView: LinkTextView {
// todo: should be weak weak var navigationDelegate: TuskerNavigationDelegate?
var navigationDelegate: TuskerNavigationDelegate?
var mastodonController: MastodonController? { navigationDelegate?.apiController } var mastodonController: MastodonController? { navigationDelegate?.apiController }
var defaultFont: UIFont = .systemFont(ofSize: 17) var defaultFont: UIFont = .systemFont(ofSize: 17)

View File

@ -11,7 +11,7 @@ import Pachyderm
class HashtagTableViewCell: UITableViewCell { class HashtagTableViewCell: UITableViewCell {
var delegate: TuskerNavigationDelegate? weak var delegate: TuskerNavigationDelegate?
@IBOutlet weak var hashtagLabel: UILabel! @IBOutlet weak var hashtagLabel: UILabel!

View File

@ -12,7 +12,7 @@ import SwiftSoup
class ActionNotificationGroupTableViewCell: UITableViewCell { class ActionNotificationGroupTableViewCell: UITableViewCell {
var delegate: TuskerNavigationDelegate? weak var delegate: TuskerNavigationDelegate?
var mastodonController: MastodonController! { delegate?.apiController } var mastodonController: MastodonController! { delegate?.apiController }
@IBOutlet weak var actionImageView: UIImageView! @IBOutlet weak var actionImageView: UIImageView!
@ -27,6 +27,10 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
var authorAvatarURL: URL? var authorAvatarURL: URL?
var updateTimestampWorkItem: DispatchWorkItem? var updateTimestampWorkItem: DispatchWorkItem?
deinit {
updateTimestampWorkItem?.cancel()
}
override func awakeFromNib() { override func awakeFromNib() {
super.awakeFromNib() super.awakeFromNib()
@ -110,7 +114,9 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
delay = nil delay = nil
} }
if let delay = delay { if let delay = delay {
updateTimestampWorkItem = DispatchWorkItem(block: self.updateTimestamp) updateTimestampWorkItem = DispatchWorkItem { [unowned self] in
self.updateTimestamp()
}
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: updateTimestampWorkItem!) DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: updateTimestampWorkItem!)
} else { } else {
updateTimestampWorkItem = nil updateTimestampWorkItem = nil

View File

@ -11,7 +11,7 @@ import Pachyderm
class FollowNotificationGroupTableViewCell: UITableViewCell { class FollowNotificationGroupTableViewCell: UITableViewCell {
var delegate: TuskerNavigationDelegate? weak var delegate: TuskerNavigationDelegate?
var mastodonController: MastodonController! { delegate?.apiController } var mastodonController: MastodonController! { delegate?.apiController }
@IBOutlet weak var avatarStackView: UIStackView! @IBOutlet weak var avatarStackView: UIStackView!
@ -22,6 +22,10 @@ class FollowNotificationGroupTableViewCell: UITableViewCell {
var updateTimestampWorkItem: DispatchWorkItem? var updateTimestampWorkItem: DispatchWorkItem?
deinit {
updateTimestampWorkItem?.cancel()
}
override func awakeFromNib() { override func awakeFromNib() {
super.awakeFromNib() super.awakeFromNib()
@ -98,7 +102,7 @@ class FollowNotificationGroupTableViewCell: UITableViewCell {
delay = nil delay = nil
} }
if let delay = delay { if let delay = delay {
updateTimestampWorkItem = DispatchWorkItem { updateTimestampWorkItem = DispatchWorkItem { [unowned self] in
self.updateTimestamp() self.updateTimestamp()
} }
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: updateTimestampWorkItem!) DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: updateTimestampWorkItem!)

View File

@ -11,7 +11,7 @@ import Pachyderm
class FollowRequestNotificationTableViewCell: UITableViewCell { class FollowRequestNotificationTableViewCell: UITableViewCell {
var delegate: TuskerNavigationDelegate? weak var delegate: TuskerNavigationDelegate?
var mastodonController: MastodonController! { delegate?.apiController } var mastodonController: MastodonController! { delegate?.apiController }
@IBOutlet weak var stackView: UIStackView! @IBOutlet weak var stackView: UIStackView!
@ -27,6 +27,10 @@ class FollowRequestNotificationTableViewCell: UITableViewCell {
var updateTimestampWorkItem: DispatchWorkItem? var updateTimestampWorkItem: DispatchWorkItem?
deinit {
updateTimestampWorkItem?.cancel()
}
override func awakeFromNib() { override func awakeFromNib() {
super.awakeFromNib() super.awakeFromNib()
@ -72,7 +76,9 @@ class FollowRequestNotificationTableViewCell: UITableViewCell {
delay = nil delay = nil
} }
if let delay = delay { if let delay = delay {
updateTimestampWorkItem = DispatchWorkItem(block: self.updateTimestamp) updateTimestampWorkItem = DispatchWorkItem { [unowned self] in
self.updateTimestamp()
}
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: updateTimestampWorkItem!) DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: updateTimestampWorkItem!)
} else { } else {
updateTimestampWorkItem = nil updateTimestampWorkItem = nil

View File

@ -15,7 +15,7 @@ protocol ProfileHeaderTableViewCellDelegate: TuskerNavigationDelegate {
class ProfileHeaderTableViewCell: UITableViewCell { class ProfileHeaderTableViewCell: UITableViewCell {
var delegate: ProfileHeaderTableViewCellDelegate? weak var delegate: ProfileHeaderTableViewCellDelegate?
var mastodonController: MastodonController! { delegate?.apiController } var mastodonController: MastodonController! { delegate?.apiController }
@IBOutlet weak var headerImageView: UIImageView! @IBOutlet weak var headerImageView: UIImageView!

View File

@ -16,7 +16,7 @@ protocol StatusTableViewCellDelegate: TuskerNavigationDelegate {
class BaseStatusTableViewCell: UITableViewCell { class BaseStatusTableViewCell: UITableViewCell {
var delegate: StatusTableViewCellDelegate? { weak var delegate: StatusTableViewCellDelegate? {
didSet { didSet {
contentTextView.navigationDelegate = delegate contentTextView.navigationDelegate = delegate
} }
@ -100,16 +100,16 @@ class BaseStatusTableViewCell: UITableViewCell {
open func createObserversIfNecessary() { open func createObserversIfNecessary() {
if statusUpdater == nil { if statusUpdater == nil {
statusUpdater = mastodonController.cache.statusSubject statusUpdater = mastodonController.cache.statusSubject
.filter { $0.id == self.statusID } .filter { [unowned self] in $0.id == self.statusID }
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink(receiveValue: updateStatusState(status:)) .sink { [unowned self] in self.updateStatusState(status: $0) }
} }
if accountUpdater == nil { if accountUpdater == nil {
accountUpdater = mastodonController.cache.accountSubject accountUpdater = mastodonController.cache.accountSubject
.filter { $0.id == self.accountID } .filter { [unowned self] in $0.id == self.accountID }
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink(receiveValue: updateUI(account:)) .sink { [unowned self] in self.updateUI(account: $0) }
} }
} }

View File

@ -34,6 +34,7 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
deinit { deinit {
rebloggerAccountUpdater?.cancel() rebloggerAccountUpdater?.cancel()
updateTimestampWorkItem?.cancel()
} }
override func awakeFromNib() { override func awakeFromNib() {
@ -48,9 +49,9 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
if rebloggerAccountUpdater == nil { if rebloggerAccountUpdater == nil {
rebloggerAccountUpdater = mastodonController.cache.accountSubject rebloggerAccountUpdater = mastodonController.cache.accountSubject
.filter { $0.id == self.rebloggerID } .filter { [unowned self] in $0.id == self.rebloggerID }
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink(receiveValue: updateRebloggerLabel(reblogger:)) .sink { [unowned self] in self.updateRebloggerLabel(reblogger: $0) }
} }
} }
@ -94,6 +95,7 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
} }
func updateTimestamp() { func updateTimestamp() {
guard superview != nil else { return }
guard let status = mastodonController.cache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") } guard let status = mastodonController.cache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") }
timestampLabel.text = status.createdAt.timeAgoString() timestampLabel.text = status.createdAt.timeAgoString()
@ -109,7 +111,7 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
delay = nil delay = nil
} }
if let delay = delay { if let delay = delay {
updateTimestampWorkItem = DispatchWorkItem { updateTimestampWorkItem = DispatchWorkItem { [unowned self] in
self.updateTimestamp() self.updateTimestamp()
} }
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: updateTimestampWorkItem!) DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: updateTimestampWorkItem!)