diff --git a/Tusker/CoreData/MastodonCachePersistentStore.swift b/Tusker/CoreData/MastodonCachePersistentStore.swift index 81dc89e1..33b4acfc 100644 --- a/Tusker/CoreData/MastodonCachePersistentStore.swift +++ b/Tusker/CoreData/MastodonCachePersistentStore.swift @@ -37,17 +37,23 @@ class MastodonCachePersistentStore: NSPersistentContainer { } } - private func upsert(status: Status) { + private func upsert(status: Status, incrementReferenceCount: Bool) { if let statusMO = self.status(for: status.id, in: self.backgroundContext) { statusMO.updateFrom(apiStatus: status, container: self) + if incrementReferenceCount { + statusMO.incrementReferenceCount() + } } else { - _ = StatusMO(apiStatus: status, container: self, context: self.backgroundContext) + let statusMO = StatusMO(apiStatus: status, container: self, context: self.backgroundContext) + if incrementReferenceCount { + statusMO.incrementReferenceCount() + } } } - func addOrUpdate(status: Status, save: Bool = true) { + func addOrUpdate(status: Status, incrementReferenceCount: Bool, save: Bool = true) { backgroundContext.perform { - self.upsert(status: status) + self.upsert(status: status, incrementReferenceCount: incrementReferenceCount) if save, self.backgroundContext.hasChanges { try! self.backgroundContext.save() } @@ -56,7 +62,7 @@ class MastodonCachePersistentStore: NSPersistentContainer { func addAll(statuses: [Status], completion: (() -> Void)? = nil) { backgroundContext.perform { - statuses.forEach(self.upsert(status:)) + statuses.forEach { self.upsert(status: $0, incrementReferenceCount: true) } if self.backgroundContext.hasChanges { try! self.backgroundContext.save() } diff --git a/Tusker/CoreData/StatusMO.swift b/Tusker/CoreData/StatusMO.swift index aed68312..f413ee78 100644 --- a/Tusker/CoreData/StatusMO.swift +++ b/Tusker/CoreData/StatusMO.swift @@ -35,6 +35,7 @@ public final class StatusMO: NSManagedObject { @NSManaged public var pinned: Bool @NSManaged public var reblogged: Bool @NSManaged public var reblogsCount: Int + @NSManaged public var referenceCount: Int @NSManaged public var sensitive: Bool @NSManaged public var spoilerText: String @NSManaged public var uri: String // todo: are both uri and url necessary? @@ -64,12 +65,30 @@ public final class StatusMO: NSManagedObject { } } + func incrementReferenceCount() { + referenceCount += 1 + } + + func decrementReferenceCount() { + referenceCount -= 1 + if referenceCount <= 0 { + managedObjectContext!.delete(self) + } + } + + public override func prepareForDeletion() { + super.prepareForDeletion() + reblog?.decrementReferenceCount() + } + } extension StatusMO { convenience init(apiStatus status: Pachyderm.Status, container: MastodonCachePersistentStore, context: NSManagedObjectContext) { self.init(context: context) self.updateFrom(apiStatus: status, container: container) + + reblog?.incrementReferenceCount() } func updateFrom(apiStatus status: Pachyderm.Status, container: MastodonCachePersistentStore) { diff --git a/Tusker/CoreData/Tusker.xcdatamodeld/Tusker.xcdatamodel/contents b/Tusker/CoreData/Tusker.xcdatamodeld/Tusker.xcdatamodel/contents index 67b5f931..c4e2508a 100644 --- a/Tusker/CoreData/Tusker.xcdatamodeld/Tusker.xcdatamodel/contents +++ b/Tusker/CoreData/Tusker.xcdatamodeld/Tusker.xcdatamodel/contents @@ -43,6 +43,7 @@ + @@ -58,6 +59,6 @@ - + \ No newline at end of file diff --git a/Tusker/MastodonCache.swift b/Tusker/MastodonCache.swift index 2d50bee4..9aec17c6 100644 --- a/Tusker/MastodonCache.swift +++ b/Tusker/MastodonCache.swift @@ -59,12 +59,10 @@ class MastodonCache { func add(status: Status) { set(status: status, for: status.id) - mastodonController?.persistentContainer.addOrUpdate(status: status) } func addAll(statuses: [Status]) { statuses.forEach(add) - mastodonController?.persistentContainer.addAll(statuses: statuses) } // MARK: - Accounts @@ -94,12 +92,10 @@ class MastodonCache { func add(account: Account) { set(account: account, for: account.id) - mastodonController?.persistentContainer.addOrUpdate(account: account) } func addAll(accounts: [Account]) { accounts.forEach(add) - mastodonController?.persistentContainer.addAll(accounts: accounts) } // MARK: - Relationships diff --git a/Tusker/SceneDelegate.swift b/Tusker/SceneDelegate.swift index 5755676f..10eba605 100644 --- a/Tusker/SceneDelegate.swift +++ b/Tusker/SceneDelegate.swift @@ -109,6 +109,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // Called as the scene transitions from the foreground to the background. // Use this method to save data, release shared resources, and store enough scene-specific state information // to restore the scene back to its current state. + + try! scene.session.mastodonController?.persistentContainer.viewContext.save() } func activateAccount(_ account: LocalData.UserAccountInfo) { diff --git a/Tusker/Screens/Timeline/TimelineTableViewController.swift b/Tusker/Screens/Timeline/TimelineTableViewController.swift index 8d07441e..e10f83f8 100644 --- a/Tusker/Screens/Timeline/TimelineTableViewController.swift +++ b/Tusker/Screens/Timeline/TimelineTableViewController.swift @@ -19,6 +19,9 @@ class TimelineTableViewController: EnhancedTableViewController { var newer: RequestRange? var older: RequestRange? + private var prevScrollViewContentOffset: CGPoint? + private var scrollViewDirection: CGFloat = 0 + init(for timeline: Timeline, mastodonController: MastodonController) { self.timeline = timeline self.mastodonController = mastodonController @@ -38,6 +41,17 @@ class TimelineTableViewController: EnhancedTableViewController { fatalError("init(coder:) has not been implemented") } + deinit { + // decrement reference counts of any statuses we still have + // if the app is currently being quit, this will not affect the persisted data because + // the persistent container would already have been saved in SceneDelegate.sceneDidEnterBackground(_:) + for segment in timelineSegments { + for (id, _) in segment { + mastodonController.persistentContainer.status(for: id)?.decrementReferenceCount() + } + } + } + func statusID(for indexPath: IndexPath) -> String { return timelineSegments[indexPath.section][indexPath.row].id } @@ -59,7 +73,6 @@ class TimelineTableViewController: EnhancedTableViewController { let request = Client.getStatuses(timeline: timeline) mastodonController.run(request) { response in guard case let .success(statuses, pagination) = response else { fatalError() } -// self.mastodonController.cache.addAll(statuses: statuses) // todo: possible race condition here? we update the underlying data before waiting to reload the table view self.timelineSegments.insert(statuses.map { ($0.id, .unknown) }, at: 0) self.newer = pagination?.newer @@ -96,6 +109,59 @@ class TimelineTableViewController: EnhancedTableViewController { // MARK: - Table view delegate override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { + // when scrolling upwards, decrement reference counts for old statuses, if necessary + if scrollViewDirection < 0 { + if indexPath.section <= timelineSegments.count - 2 { + // decrement ref counts for all sections below the section below the current section + // (e.g., there exist sections 0, 1, 2 and we're currently scrolling upwards in section 0, we want to remove section 2) + + // todo: this is in the hot path for scrolling, possibly move this to a background thread? + let sectionsToRemove = indexPath.section + 1.. 2 * pageSize, + indexPath.row < lastSection.count - (2 * pageSize) { + // todo: this is in the hot path for scrolling, possibly move this to a background thread? + let statusesToRemove = lastSection[lastSection.count - pageSize..