iOS client for Grain grain.social
ios photography atproto
7
fork

Configure Feed

Select the types of activity you want to include in your feed.

debug: add signposts to trace sheet collapse + timer restart

Adds Logger and OSSignposter instrumentation at every event that could
explain the chained failure: body evaluation, .task fires, presentStories,
advanceStory, loadStoriesForCurrentAuthor, startTimerIfSafe, timer
start/stop/complete, openCommentSheet, sheet content body evaluation
(logs whether sheetStoryUri or currentStory is nil), image onAppear
(both cached and lazy paths), body onAppear/onDisappear, and onChange
for showCommentSheet / imageLoaded / currentStoryIndex / stories.count.

Subsystem: social.grain.grain, category: StoryViewer.
Filter in Console.app or use `log stream` with predicate:
subsystem == "social.grain.grain" AND category == "StoryViewer"

+62 -4
+62 -4
Grain/Views/Stories/StoryViewer.swift
··· 1 1 import Nuke 2 2 import NukeUI 3 + import os 3 4 import SwiftUI 5 + 6 + private let svLogger = Logger(subsystem: "social.grain.grain", category: "StoryViewer") 7 + private let svSignposter = OSSignposter(subsystem: "social.grain.grain", category: "StoryViewer") 4 8 5 9 @Observable 6 10 @MainActor ··· 11 15 private let duration: TimeInterval = 5.0 12 16 13 17 func start() { 18 + svLogger.info("[timer.start] called") 19 + svSignposter.emitEvent("timer.start") 14 20 stop() 15 21 progress = 0 16 22 isRunning = true ··· 31 37 } 32 38 guard !Task.isCancelled else { return } 33 39 isRunning = false 40 + svLogger.info("[timer.complete] fired onComplete") 41 + svSignposter.emitEvent("timer.complete") 34 42 onComplete?() 35 43 } 36 44 } 37 45 38 46 func stop() { 47 + if isRunning { 48 + svLogger.info("[timer.stop] called (was running)") 49 + svSignposter.emitEvent("timer.stop") 50 + } 39 51 task?.cancel() 40 52 task = nil 41 53 isRunning = false ··· 135 147 } 136 148 137 149 var body: some View { 138 - ZStack { 150 + let _ = svLogger.debug("[body] eval showCommentSheet=\(showCommentSheet) storiesCount=\(stories.count) currentIdx=\(currentStoryIndex) imageLoaded=\(imageLoaded)") 151 + return ZStack { 139 152 if let pendingIdx = pendingTransition.authorIndex { 140 153 pendingFaceView(authorIdx: pendingIdx) 141 154 .offset(x: faceOffsets.pending) ··· 154 167 .transition(.identity) 155 168 } 156 169 } 170 + .onAppear { svLogger.info("[body] onAppear") } 171 + .onDisappear { svLogger.info("[body] onDisappear") } 157 172 .clipped() 158 173 .background(Color.black.ignoresSafeArea()) 159 174 .background( ··· 187 202 if reportTarget == nil { timer.start() } 188 203 } 189 204 .sheet(isPresented: $showCommentSheet) { 205 + let _ = svLogger.info("[sheet.content] body eval sheetStoryUri=\(sheetStoryUri ?? "nil") currentStoryURI=\(currentStory?.uri ?? "nil")") 190 206 if let uri = sheetStoryUri { 191 207 StoryCommentSheet( 192 208 viewModel: commentsViewModel, ··· 203 219 .environment(auth) 204 220 .environment(storyStatusCache) 205 221 .environment(viewedStories) 222 + .onAppear { svLogger.info("[sheet.content] onAppear uri=\(uri)") } 223 + .onDisappear { svLogger.info("[sheet.content] onDisappear") } 224 + } else { 225 + let _ = svLogger.info("[sheet.content] EMPTY (sheetStoryUri nil)") 206 226 } 207 227 } 228 + .onChange(of: showCommentSheet) { old, new in 229 + svLogger.info("[onChange showCommentSheet] \(old) → \(new) sheetStoryUri=\(sheetStoryUri ?? "nil")") 230 + } 231 + .onChange(of: imageLoaded) { old, new in 232 + svLogger.info("[onChange imageLoaded] \(old) → \(new)") 233 + } 234 + .onChange(of: currentStoryIndex) { old, new in 235 + svLogger.info("[onChange currentStoryIndex] \(old) → \(new)") 236 + } 237 + .onChange(of: stories.count) { old, new in 238 + svLogger.info("[onChange stories.count] \(old) → \(new)") 239 + } 208 240 .task { 241 + svLogger.info("[task] fired hasLoadedInitialStories=\(hasLoadedInitialStories) showCommentSheet=\(showCommentSheet)") 242 + svSignposter.emitEvent("task.fired", "hasLoaded=\(hasLoadedInitialStories),sheetOpen=\(showCommentSheet)") 209 243 // Guard against re-runs: .task can re-fire when the view re-enters the 210 244 // hierarchy (e.g. after sheet presentation cycles), and we only want to 211 245 // load stories once per StoryViewer instance. 212 - guard !hasLoadedInitialStories else { return } 246 + guard !hasLoadedInitialStories else { 247 + svLogger.info("[task] skipping re-run") 248 + return 249 + } 213 250 hasLoadedInitialStories = true 214 251 if isPreview, prefetchedStories.isEmpty { return } 215 252 let startAuthor = authors[currentAuthorIndex] ··· 221 258 timer.onQuarter = { [self] in markCurrentStoryViewed() } 222 259 } 223 260 await loadStoriesForCurrentAuthor() 261 + svLogger.info("[task] done") 224 262 } 225 263 } 226 264 ··· 377 415 // Text("fullsize · cache").font(.caption2.bold()).padding(6).background(.black.opacity(0.5)).foregroundStyle(.white).padding(8) 378 416 // } 379 417 .onAppear { 418 + svLogger.info("[image.onAppear cached-fullsize] imageLoaded=\(imageLoaded) showCommentSheet=\(showCommentSheet)") 380 419 if !imageLoaded { 381 420 imageLoaded = true 382 421 startTimerIfSafe() ··· 399 438 // Text("fullsize · network").font(.caption2.bold()).padding(6).background(.black.opacity(0.5)).foregroundStyle(.white).padding(8) 400 439 // } 401 440 .onAppear { 441 + svLogger.info("[image.onAppear lazy-fullsize] imageLoaded=\(imageLoaded) showCommentSheet=\(showCommentSheet)") 402 442 if !imageLoaded { 403 443 imageLoaded = true 404 444 startTimerIfSafe() ··· 627 667 } 628 668 629 669 private func startTimerIfSafe() { 630 - guard imageLoaded else { return } 670 + guard imageLoaded else { 671 + svLogger.info("[startTimerIfSafe] skipped (imageLoaded=false)") 672 + return 673 + } 631 674 let action = storyLabelResult.action 675 + svLogger.info("[startTimerIfSafe] action=\(String(describing: action)) showCommentSheet=\(showCommentSheet)") 632 676 if action == .none || action == .badge { timer.start() } 633 677 } 634 678 ··· 660 704 } 661 705 662 706 private func advanceStory(by delta: Int) { 707 + svLogger.info("[advanceStory] delta=\(delta) currentIdx=\(currentStoryIndex)") 708 + svSignposter.emitEvent("advanceStory", "delta=\(delta)") 663 709 timer.progress = 0 664 710 let newIndex = currentStoryIndex + delta 665 711 let nextStory = stories.indices.contains(newIndex) ? stories[newIndex] : nil ··· 855 901 } 856 902 857 903 private func loadStoriesForCurrentAuthor() async { 904 + svLogger.info("[loadStoriesForCurrentAuthor] enter authorIdx=\(currentAuthorIndex)") 905 + svSignposter.emitEvent("loadStoriesForCurrentAuthor.enter") 858 906 guard currentAuthorIndex < authors.count else { return } 859 907 let did = authors[currentAuthorIndex].profile.did 860 908 isLoadingStories = true 861 909 timer.stop() 862 910 863 911 do { 912 + let fromCache = prefetchedStories[did] != nil 864 913 let fetched: [GrainStory] = if let cached = prefetchedStories.removeValue(forKey: did) { 865 914 cached 866 915 } else { 867 916 try await client.getStories(actor: did, auth: auth.authContext()).stories 868 917 } 918 + svLogger.info("[loadStoriesForCurrentAuthor] fetched count=\(fetched.count) fromCache=\(fromCache)") 869 919 presentStories(fetched) 870 920 } catch { 921 + svLogger.error("[loadStoriesForCurrentAuthor] error: \(error)") 871 922 stories = [] 872 923 isLoadingStories = false 873 924 } 874 925 } 875 926 876 927 private func presentStories(_ fetched: [GrainStory], resumeIndex: Int? = nil) { 928 + svLogger.info("[presentStories] enter count=\(fetched.count) resumeIndex=\(resumeIndex ?? -1) showCommentSheet=\(showCommentSheet)") 929 + svSignposter.emitEvent("presentStories.enter", "count=\(fetched.count),sheetOpen=\(showCommentSheet)") 877 930 let targetIndex: Int 878 931 if let resume = resumeIndex { 879 932 targetIndex = min(resume, max(fetched.count - 1, 0)) ··· 1002 1055 /// content doesn't depend on a live reading of `currentStory` (which can 1003 1056 /// flicker to nil during view re-renders). 1004 1057 private func openCommentSheet(focusInput: Bool) { 1005 - guard let uri = currentStory?.uri else { return } 1058 + guard let uri = currentStory?.uri else { 1059 + svLogger.info("[openCommentSheet] SKIPPED (currentStory nil)") 1060 + return 1061 + } 1062 + svLogger.info("[openCommentSheet] uri=\(uri) focusInput=\(focusInput)") 1063 + svSignposter.emitEvent("openCommentSheet", "focusInput=\(focusInput)") 1006 1064 timer.stop() 1007 1065 sheetStoryUri = uri 1008 1066 commentSheetFocusInput = focusInput