From f40eabdcbd539c7b1ae08953edda39a441cf4f3f Mon Sep 17 00:00:00 2001 From: vanities Date: Sat, 12 Jul 2025 09:21:05 -0500 Subject: [PATCH 1/5] Add debug logging for thread position --- swiftchan/Environment/UserSettings.swift | 46 ++++++++++ .../Boards/Catalog/Thread/ThreadView.swift | 88 +++++++++++++++++++ swiftchan/Views/SettingsView.swift | 1 + 3 files changed, 135 insertions(+) diff --git a/swiftchan/Environment/UserSettings.swift b/swiftchan/Environment/UserSettings.swift index 7814e0d..0e12669 100644 --- a/swiftchan/Environment/UserSettings.swift +++ b/swiftchan/Environment/UserSettings.swift @@ -54,6 +54,52 @@ extension UserDefaults { return UserDefaults.standard.bool(forKey: "hiddenPosts board=\(boardName) postId=\(postId)") } + static func getThreadPosition(boardName: String, threadId: Int) -> Int? { + let key = "threadPosition board=\(boardName) thread=\(threadId)" + if UserDefaults.standard.object(forKey: key) == nil { + print("No saved thread position for \(key)") + return nil + } + let value = UserDefaults.standard.integer(forKey: key) + print("Retrieved thread position \(value) for \(key)") + return value + } + + static func getThreadOffset(boardName: String, threadId: Int) -> Double? { + let key = "threadOffset board=\(boardName) thread=\(threadId)" + if UserDefaults.standard.object(forKey: key) == nil { + print("No saved thread offset for \(key)") + return nil + } + let value = UserDefaults.standard.double(forKey: key) + print("Retrieved thread offset \(value) for \(key)") + return value + } + + static func setThreadPosition(boardName: String, threadId: Int, index: Int) { + let key = "threadPosition board=\(boardName) thread=\(threadId)" + print("Saving thread position \(index) for \(key)") + UserDefaults.standard.set(index, forKey: key) + } + + static func setThreadOffset(boardName: String, threadId: Int, offset: Double) { + let key = "threadOffset board=\(boardName) thread=\(threadId)" + print("Saving thread offset \(offset) for \(key)") + UserDefaults.standard.set(offset, forKey: key) + } + + static func removeThreadPosition(boardName: String, threadId: Int) { + let key = "threadPosition board=\(boardName) thread=\(threadId)" + print("Removing thread position for \(key)") + UserDefaults.standard.removeObject(forKey: key) + } + + static func removeThreadOffset(boardName: String, threadId: Int) { + let key = "threadOffset board=\(boardName) thread=\(threadId)" + print("Removing thread offset for \(key)") + UserDefaults.standard.removeObject(forKey: key) + } + // MARK: Setters static func setDidUnlokcBiometrics(value: Bool) { UserDefaults.standard.set(value, forKey: "didUnlokcBiometrics") diff --git a/swiftchan/Views/Boards/Catalog/Thread/ThreadView.swift b/swiftchan/Views/Boards/Catalog/Thread/ThreadView.swift index 818e057..fa27486 100644 --- a/swiftchan/Views/Boards/Catalog/Thread/ThreadView.swift +++ b/swiftchan/Views/Boards/Catalog/Thread/ThreadView.swift @@ -9,6 +9,7 @@ import SwiftUI import FourChan import Combine import SpriteKit +import UIKit func createThreadUpdateTimer() -> Publishers.Autoconnect { return Timer.publish(every: 1, on: .current, in: .common).autoconnect() @@ -18,6 +19,7 @@ struct ThreadView: View { @AppStorage("autoRefreshEnabled") private var autoRefreshEnabled = true @AppStorage("autoRefreshThreadTime") private var autoRefreshThreadTime = 10 @AppStorage("hideTabOnBoards") var hideTabOnBoards = false + @AppStorage("rememberThreadPositions") var rememberThreadPositions = true @Environment(\.scenePhase) private var scenePhase @Environment(AppState.self) private var appState @@ -29,6 +31,11 @@ struct ThreadView: View { @State private var replyId: Int = 0 @State private var showThread: Bool = false @State private var threadDestination = ThreadDestination(board: "", id: 0) + @State private var savedIndex: Int? + @State private var lastVisibleIndex: Int? + @State private var savedOffset: CGFloat? + @State private var scrollViewRef: UIScrollView? + @State private var didRestoreOffset = false var scene: SKScene { let scene = SnowScene() @@ -47,6 +54,15 @@ struct ThreadView: View { id: postNumber ) ) + let savedIndex = UserDefaults.getThreadPosition(boardName: boardName, threadId: Int(postNumber)) + self._savedIndex = State(wrappedValue: savedIndex) + + if let offset = UserDefaults.getThreadOffset(boardName: boardName, threadId: Int(postNumber)) { + self._savedOffset = State(wrappedValue: CGFloat(offset)) + } else { + self._savedOffset = State(initialValue: nil) + } + print("ThreadView init: board=\(boardName), threadId=\(postNumber), savedIndex=\(String(describing: savedIndex)), savedOffset=\(String(describing: _savedOffset.wrappedValue))") } @ViewBuilder @@ -75,6 +91,10 @@ struct ThreadView: View { if !post.isHidden(boardName: viewModel.boardName) { PostView(index: postIndex) .environment(viewModel) + .onAppear { + lastVisibleIndex = postIndex + print("Last visible index updated to \(postIndex)") + } } } } @@ -86,6 +106,19 @@ struct ThreadView: View { } } .opacity(opacity) + .introspect(.scrollView, on: .iOS(.v17)) { scrollView in + scrollViewRef = scrollView + print("Got scroll view reference") + restoreSavedPosition(reader: reader) + } + } + .onAppear { + print("ScrollView onAppear") + restoreSavedPosition(reader: reader) + } + .onChange(of: viewModel.posts.count) { _, _ in + print("Posts count changed, attempting to restore") + restoreSavedPosition(reader: reader) } } } @@ -112,9 +145,11 @@ struct ThreadView: View { .environment(presentationState) .environment(viewModel) .onAppear { + print("Gallery onAppear - canceling autorefresh") threadAutorefresher.cancelTimer() } .onDisappear { + print("Gallery onDisappear - starting autorefresh") threadAutorefresher.setTimer() } } @@ -132,10 +167,34 @@ struct ThreadView: View { } } .onAppear { + print("ThreadView onAppear - prefetching") viewModel.prefetch() } .onDisappear { viewModel.stopPrefetching() + if rememberThreadPositions { + if let index = lastVisibleIndex { + print("Saving index \(index)") + UserDefaults.setThreadPosition( + boardName: viewModel.boardName, + threadId: viewModel.id, + index: index + ) + } + if let scrollView = scrollViewRef { + let offset = scrollView.contentOffset.y + print("Saving offset \(offset)") + UserDefaults.setThreadOffset( + boardName: viewModel.boardName, + threadId: viewModel.id, + offset: Double(offset) + ) + } + } else { + print("Removing saved position and offset") + UserDefaults.removeThreadPosition(boardName: viewModel.boardName, threadId: viewModel.id) + UserDefaults.removeThreadOffset(boardName: viewModel.boardName, threadId: viewModel.id) + } } .onReceive(threadAutorefresher.timer) { _ in if threadAutorefresher.incrementRefreshTimer() { @@ -237,6 +296,35 @@ struct ThreadView: View { viewModel.prefetch() } + private func restoreSavedPosition(reader: ScrollViewProxy) { + guard rememberThreadPositions, let scrollView = scrollViewRef else { return } + guard !didRestoreOffset else { return } + + print("Attempting restore: savedOffset=\(String(describing: savedOffset)), savedIndex=\(String(describing: savedIndex))") + + if let offset = savedOffset { + print("Restoring using offset \(offset)") + applyOffset(offset, to: scrollView) + didRestoreOffset = true + } else if let index = savedIndex, index < viewModel.posts.count { + print("Restoring using index \(index)") + DispatchQueue.main.async { + reader.scrollTo(index, anchor: .top) + didRestoreOffset = true + } + } + } + + private func applyOffset(_ offset: CGFloat, to scrollView: UIScrollView) { + let delays = [0.0, 0.1, 0.4] + for delay in delays { + DispatchQueue.main.asyncAfter(deadline: .now() + delay) { + print("Applying offset \(offset) after delay \(delay)") + scrollView.setContentOffset(CGPoint(x: 0, y: offset), animated: false) + } + } + } + private func scrollToPost(reader: ScrollViewProxy) { if presentationState.presentingIndex != presentationState.galleryIndex, let mediaI = viewModel.postMediaMapping.firstIndex(where: { $0.value == presentationState.galleryIndex }) { diff --git a/swiftchan/Views/SettingsView.swift b/swiftchan/Views/SettingsView.swift index 26cd7bf..465e1be 100644 --- a/swiftchan/Views/SettingsView.swift +++ b/swiftchan/Views/SettingsView.swift @@ -67,6 +67,7 @@ struct SettingsView: View { var threadSection: some View { Section(header: Text("Thread").font(.title)) { Toggle("Auto Refresh Enabled", isOn: $autoRefreshEnabled) + Toggle("Remember Thread Position", isOn: $rememberThreadPositions) HStack { Text("Auto Refresh Time") Spacer() From 581e62615a9fd796fec1d9a16b63794e8dc0a929 Mon Sep 17 00:00:00 2001 From: vanities Date: Sat, 12 Jul 2025 09:38:29 -0500 Subject: [PATCH 2/5] Improve scroll restoration --- .../Boards/Catalog/Thread/ThreadView.swift | 55 +++++++++++-------- 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/swiftchan/Views/Boards/Catalog/Thread/ThreadView.swift b/swiftchan/Views/Boards/Catalog/Thread/ThreadView.swift index fa27486..3d3574f 100644 --- a/swiftchan/Views/Boards/Catalog/Thread/ThreadView.swift +++ b/swiftchan/Views/Boards/Catalog/Thread/ThreadView.swift @@ -172,28 +172,11 @@ struct ThreadView: View { } .onDisappear { viewModel.stopPrefetching() - if rememberThreadPositions { - if let index = lastVisibleIndex { - print("Saving index \(index)") - UserDefaults.setThreadPosition( - boardName: viewModel.boardName, - threadId: viewModel.id, - index: index - ) - } - if let scrollView = scrollViewRef { - let offset = scrollView.contentOffset.y - print("Saving offset \(offset)") - UserDefaults.setThreadOffset( - boardName: viewModel.boardName, - threadId: viewModel.id, - offset: Double(offset) - ) - } - } else { - print("Removing saved position and offset") - UserDefaults.removeThreadPosition(boardName: viewModel.boardName, threadId: viewModel.id) - UserDefaults.removeThreadOffset(boardName: viewModel.boardName, threadId: viewModel.id) + savePosition() + } + .onChange(of: scenePhase) { _, newPhase in + if newPhase != .active { + savePosition() } } .onReceive(threadAutorefresher.timer) { _ in @@ -315,8 +298,34 @@ struct ThreadView: View { } } + private func savePosition() { + if rememberThreadPositions { + if let index = lastVisibleIndex { + print("Saving index \(index)") + UserDefaults.setThreadPosition( + boardName: viewModel.boardName, + threadId: viewModel.id, + index: index + ) + } + if let scrollView = scrollViewRef { + let offset = scrollView.contentOffset.y + print("Saving offset \(offset)") + UserDefaults.setThreadOffset( + boardName: viewModel.boardName, + threadId: viewModel.id, + offset: Double(offset) + ) + } + } else { + print("Removing saved position and offset") + UserDefaults.removeThreadPosition(boardName: viewModel.boardName, threadId: viewModel.id) + UserDefaults.removeThreadOffset(boardName: viewModel.boardName, threadId: viewModel.id) + } + } + private func applyOffset(_ offset: CGFloat, to scrollView: UIScrollView) { - let delays = [0.0, 0.1, 0.4] + let delays: [Double] = [0.0, 0.1, 0.4, 0.8, 1.2] for delay in delays { DispatchQueue.main.asyncAfter(deadline: .now() + delay) { print("Applying offset \(offset) after delay \(delay)") From a5e48d27af1f7c600f29702191911df32b6e35a6 Mon Sep 17 00:00:00 2001 From: vanities Date: Sat, 12 Jul 2025 09:46:03 -0500 Subject: [PATCH 3/5] Fix scroll position persistence --- swiftchan/Views/Boards/Catalog/Thread/ThreadView.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/swiftchan/Views/Boards/Catalog/Thread/ThreadView.swift b/swiftchan/Views/Boards/Catalog/Thread/ThreadView.swift index 3d3574f..1bb67ad 100644 --- a/swiftchan/Views/Boards/Catalog/Thread/ThreadView.swift +++ b/swiftchan/Views/Boards/Catalog/Thread/ThreadView.swift @@ -111,6 +111,10 @@ struct ThreadView: View { print("Got scroll view reference") restoreSavedPosition(reader: reader) } + .onDisappear { + print("ScrollView onDisappear - saving position") + savePosition() + } } .onAppear { print("ScrollView onAppear") @@ -316,6 +320,8 @@ struct ThreadView: View { threadId: viewModel.id, offset: Double(offset) ) + } else { + print("No scrollView reference to save offset") } } else { print("Removing saved position and offset") From 6f421944fdd0fe9bbced9e8e154a1379fd8e6a77 Mon Sep 17 00:00:00 2001 From: vanities Date: Sat, 12 Jul 2025 09:52:01 -0500 Subject: [PATCH 4/5] Fix scroll view introspection on iOS 15+ --- swiftchan/Views/Boards/Catalog/Thread/ThreadView.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/swiftchan/Views/Boards/Catalog/Thread/ThreadView.swift b/swiftchan/Views/Boards/Catalog/Thread/ThreadView.swift index 1bb67ad..2f9f96e 100644 --- a/swiftchan/Views/Boards/Catalog/Thread/ThreadView.swift +++ b/swiftchan/Views/Boards/Catalog/Thread/ThreadView.swift @@ -106,7 +106,10 @@ struct ThreadView: View { } } .opacity(opacity) - .introspect(.scrollView, on: .iOS(.v17)) { scrollView in + .introspect( + .scrollView, + on: .iOS(.v15, .v16, .v17) + ) { scrollView in scrollViewRef = scrollView print("Got scroll view reference") restoreSavedPosition(reader: reader) From 685e73c565c6ed91d495204fdaeddd4ed945f6c9 Mon Sep 17 00:00:00 2001 From: vanities Date: Sat, 12 Jul 2025 10:14:16 -0500 Subject: [PATCH 5/5] Fix scroll view introspection --- .../Boards/Catalog/Thread/ThreadView.swift | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/swiftchan/Views/Boards/Catalog/Thread/ThreadView.swift b/swiftchan/Views/Boards/Catalog/Thread/ThreadView.swift index 2f9f96e..aa29f3b 100644 --- a/swiftchan/Views/Boards/Catalog/Thread/ThreadView.swift +++ b/swiftchan/Views/Boards/Catalog/Thread/ThreadView.swift @@ -106,18 +106,18 @@ struct ThreadView: View { } } .opacity(opacity) - .introspect( - .scrollView, - on: .iOS(.v15, .v16, .v17) - ) { scrollView in - scrollViewRef = scrollView - print("Got scroll view reference") - restoreSavedPosition(reader: reader) - } - .onDisappear { - print("ScrollView onDisappear - saving position") - savePosition() - } + } + .introspect( + .scrollView, + on: .iOS(.v15, .v16, .v17) + ) { scrollView in + scrollViewRef = scrollView + print("Got scroll view reference") + restoreSavedPosition(reader: reader) + } + .onDisappear { + print("ScrollView onDisappear - saving position") + savePosition() } .onAppear { print("ScrollView onAppear")