From 7b7c05ff6898284d15957dc58351ddd4660d0ac8 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sat, 28 Jan 2023 13:41:22 -0500 Subject: [PATCH] Fix timeline position sync not working due to LazilyDecoding cache not being invalidated upon remote change --- .../MastodonCachePersistentStore.swift | 2 ++ Tusker/CoreData/TimelinePosition.swift | 4 ++++ Tusker/LazilyDecoding.swift | 24 +++++++++++++------ .../Timeline/TimelineViewController.swift | 1 + 4 files changed, 24 insertions(+), 7 deletions(-) diff --git a/Tusker/CoreData/MastodonCachePersistentStore.swift b/Tusker/CoreData/MastodonCachePersistentStore.swift index 51cb7d0b..1c6b78d7 100644 --- a/Tusker/CoreData/MastodonCachePersistentStore.swift +++ b/Tusker/CoreData/MastodonCachePersistentStore.swift @@ -545,6 +545,8 @@ class MastodonCachePersistentStore: NSPersistentCloudKitContainer { guard let timelinePosition = try? self.viewContext.existingObject(with: id) as? TimelinePosition else { continue } + // the kvo observer that clears the LazilyDecoding cache doesn't always fire on remote changes, so do it manually + timelinePosition.changedRemotely() NotificationCenter.default.post(name: .timelinePositionChanged, object: timelinePosition) } if changedAccountPrefs { diff --git a/Tusker/CoreData/TimelinePosition.swift b/Tusker/CoreData/TimelinePosition.swift index d093c114..444bae4a 100644 --- a/Tusker/CoreData/TimelinePosition.swift +++ b/Tusker/CoreData/TimelinePosition.swift @@ -41,6 +41,10 @@ public final class TimelinePosition: NSManagedObject { self.createdAt = Date() } + func changedRemotely() { + _statusIDs.removeCachedValue() + } + } // blergh, this is the simplest way of getting the Timeline into a format that A) CoreData can handle and B) is usable in the predicate diff --git a/Tusker/LazilyDecoding.swift b/Tusker/LazilyDecoding.swift index 542f1d3c..7d402eab 100644 --- a/Tusker/LazilyDecoding.swift +++ b/Tusker/LazilyDecoding.swift @@ -18,6 +18,7 @@ public struct LazilyDecoding { private let fallback: Value private var value: Value? private var observation: NSKeyValueObservation? + private var skipClearingOnNextUpdate = false init(from keyPath: ReferenceWritableKeyPath, fallback: Value) { self.keyPath = keyPath @@ -37,13 +38,16 @@ public struct LazilyDecoding { } else { guard let data = instance[keyPath: wrapper.keyPath] else { return wrapper.fallback } do { - let value = try decoder.decode(Box.self, from: data) + let value = try decoder.decode(Box.self, from: data) wrapper.value = value.value wrapper.observation = instance.observe(wrapper.keyPath, changeHandler: { instance, _ in - var updated = instance[keyPath: storageKeyPath] - updated.value = nil - updated.observation = nil - instance[keyPath: storageKeyPath] = updated + var wrapper = instance[keyPath: storageKeyPath] + if wrapper.skipClearingOnNextUpdate { + wrapper.skipClearingOnNextUpdate = false + } else { + wrapper.removeCachedValue() + } + instance[keyPath: storageKeyPath] = wrapper }) instance[keyPath: storageKeyPath] = wrapper return value.value @@ -55,12 +59,18 @@ public struct LazilyDecoding { set { var wrapper = instance[keyPath: storageKeyPath] wrapper.value = newValue + wrapper.skipClearingOnNextUpdate = true instance[keyPath: storageKeyPath] = wrapper let newData = try! encoder.encode(Box(value: newValue)) instance[keyPath: wrapper.keyPath] = newData } } + mutating func removeCachedValue() { + value = nil + observation = nil + } + } extension LazilyDecoding { @@ -72,7 +82,7 @@ extension LazilyDecoding { extension LazilyDecoding { // PropertyListEncoder only allows top-level types to be dicts or arrays, which breaks encoding nil-able values. // Wrapping everything in a Box ensures that it's always a dict. - private struct Box: Codable { - let value: T + struct Box: Codable { + let value: Value } } diff --git a/Tusker/Screens/Timeline/TimelineViewController.swift b/Tusker/Screens/Timeline/TimelineViewController.swift index 811eda5b..e5db22c4 100644 --- a/Tusker/Screens/Timeline/TimelineViewController.swift +++ b/Tusker/Screens/Timeline/TimelineViewController.swift @@ -589,6 +589,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro guard timelinePosition.centerStatusID != centerVisibleStatusID else { return false } + stateRestorationLogger.info("Potential restore with centerStatusID: \(timelinePosition.centerStatusID ?? "")") if !alwaysPrompt { Task { _ = await restoreState()