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.

chore: trim noisy StoryViewer logs + fix self in timer.resume

Removes per-body-eval, onAppear/onDisappear, onChange, task
fired/done, image.onAppear, and startTimerIfSafe/resumeTimerIfSafe
skip logs — they spammed the log stream on every SwiftUI
re-evaluation and drowned out the events that matter.

Also fixes the missing self. on StoryTimer.resume's progress
interpolation, which broke the build the moment anything else
touched this file.

+20 -62
+20 -62
Grain/Views/Stories/StoryViewer.swift
··· 132 132 @State private var hasLoadedInitialStories = false 133 133 @State private var hearts: [HeartAnimationState] = [] 134 134 @State private var isFavoriting = false 135 - @State private var likeParticleBursts: [UUID] = [] 135 + @State private var heartBeatTrigger = 0 136 136 @State private var instanceID: Int = 0 137 137 138 138 init(authors: [GrainStoryAuthor], startAuthorDid: String? = nil, initialStories: [GrainStory]? = nil, startStoryIndex: Int? = nil, client: XRPCClient, onProfileTap: ((String) -> Void)? = nil, onDismiss: (() -> Void)? = nil) { ··· 173 173 } 174 174 175 175 var body: some View { 176 - let _ = svLogger.info("[body] eval id=\(instanceID) storiesCount=\(stories.count) currentIdx=\(currentStoryIndex) imageLoaded=\(imageLoaded) hasLoaded=\(hasLoadedInitialStories)") 177 - return ZStack { 176 + ZStack { 178 177 if let pendingIdx = pendingTransition.authorIndex { 179 178 pendingFaceView(authorIdx: pendingIdx) 180 179 .offset(x: faceOffsets.pending) ··· 193 192 .transition(.identity) 194 193 } 195 194 } 196 - .onAppear { svLogger.info("[body] onAppear id=\(instanceID)") } 197 - .onDisappear { svLogger.info("[body] onDisappear id=\(instanceID)") } 198 195 .clipped() 199 196 .background(Color.black.ignoresSafeArea()) 200 197 .background( ··· 228 225 .onChange(of: reportTarget?.uri) { 229 226 if reportTarget == nil { timer.start() } 230 227 } 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)") 228 + .onChange(of: currentStory?.uri) { _, _ in 229 + hearts.removeAll() 239 230 } 240 231 .task { 241 - svLogger.info("[task] fired id=\(instanceID) hasLoadedInitialStories=\(hasLoadedInitialStories)") 242 - svSignposter.emitEvent("task.fired", "id=\(instanceID) hasLoaded=\(hasLoadedInitialStories)") 243 232 // Guard against re-runs: .task can re-fire when the view re-enters the 244 233 // hierarchy (e.g. after sheet presentation cycles), and we only want to 245 234 // load stories once per StoryViewer instance. 246 - guard !hasLoadedInitialStories else { 247 - svLogger.info("[task] skipping re-run") 248 - return 249 - } 235 + guard !hasLoadedInitialStories else { return } 250 236 hasLoadedInitialStories = true 251 237 if isPreview, prefetchedStories.isEmpty { return } 252 238 let startAuthor = authors[currentAuthorIndex] ··· 258 244 timer.onQuarter = { [self] in markCurrentStoryViewed() } 259 245 } 260 246 await loadStoriesForCurrentAuthor() 261 - svLogger.info("[task] done") 262 247 } 263 248 } 264 249 ··· 406 391 // Text("fullsize · cache").font(.caption2.bold()).padding(6).background(.black.opacity(0.5)).foregroundStyle(.white).padding(8) 407 392 // } 408 393 .onAppear { 409 - svLogger.info("[image.onAppear cached-fullsize] imageLoaded=\(imageLoaded)") 410 394 if !imageLoaded { 411 395 imageLoaded = true 412 396 startTimerIfSafe() ··· 429 413 // Text("fullsize · network").font(.caption2.bold()).padding(6).background(.black.opacity(0.5)).foregroundStyle(.white).padding(8) 430 414 // } 431 415 .onAppear { 432 - svLogger.info("[image.onAppear lazy-fullsize] imageLoaded=\(imageLoaded)") 433 416 if !imageLoaded { 434 417 imageLoaded = true 435 418 startTimerIfSafe() ··· 645 628 } 646 629 647 630 private func startTimerIfSafe() { 648 - guard imageLoaded else { 649 - svLogger.info("[startTimerIfSafe] skipped (imageLoaded=false)") 650 - return 651 - } 652 - guard !isCommentSheetOpen else { 653 - svLogger.info("[startTimerIfSafe] skipped (sheet open)") 654 - return 655 - } 631 + guard imageLoaded, !isCommentSheetOpen else { return } 656 632 let action = storyLabelResult.action 657 - svLogger.info("[startTimerIfSafe] action=\(String(describing: action))") 658 633 if action == .none || action == .badge { timer.start() } 659 634 } 660 635 661 636 private func resumeTimerIfSafe() { 662 - guard imageLoaded else { 663 - svLogger.info("[resumeTimerIfSafe] skipped (imageLoaded=false)") 664 - return 665 - } 666 - guard !isCommentSheetOpen else { 667 - svLogger.info("[resumeTimerIfSafe] skipped (sheet open)") 668 - return 669 - } 637 + guard imageLoaded, !isCommentSheetOpen else { return } 670 638 let action = storyLabelResult.action 671 - svLogger.info("[resumeTimerIfSafe] action=\(String(describing: action)) progress=\(timer.progress)") 672 639 if action == .none || action == .badge { timer.resume() } 673 640 } 674 641 ··· 1131 1098 // Heart — favorite/unfavorite 1132 1099 if interactive { 1133 1100 Button { 1134 - if !isFavorited { addLikeParticleBurst() } 1101 + if !isFavorited { heartBeatTrigger &+= 1 } 1135 1102 triggerFavoriteToggle() 1136 1103 } label: { 1137 1104 heartIcon(isFavorited: isFavorited) 1138 1105 .contentShape(Rectangle()) 1139 1106 } 1140 1107 .buttonStyle(.plain) 1141 - .overlay { particleBurstOverlay } 1142 1108 } else { 1143 1109 heartIcon(isFavorited: isFavorited) 1144 - .overlay { particleBurstOverlay } 1145 1110 .onChange(of: isFavorited) { oldValue, newValue in 1146 - if !oldValue, newValue { addLikeParticleBurst() } 1111 + if !oldValue, newValue { heartBeatTrigger &+= 1 } 1147 1112 } 1148 1113 } 1149 1114 } ··· 1156 1121 .font(.title) 1157 1122 .foregroundStyle(isFavorited ? Color("AccentColor") : .white) 1158 1123 .animation(.spring(response: 0.3, dampingFraction: 0.6), value: isFavorited) 1124 + .keyframeAnimator(initialValue: 1.0, trigger: heartBeatTrigger) { content, scale in 1125 + content.scaleEffect(scale) 1126 + } keyframes: { _ in 1127 + KeyframeTrack { 1128 + SpringKeyframe(1.35, duration: 0.14, spring: .bouncy) 1129 + SpringKeyframe(0.95, duration: 0.12, spring: .bouncy) 1130 + SpringKeyframe(1.22, duration: 0.12, spring: .bouncy) 1131 + SpringKeyframe(1.0, duration: 0.20, spring: .bouncy) 1132 + } 1133 + } 1159 1134 .frame(width: 44, height: 44) 1160 1135 } 1161 1136 1162 - private var particleBurstOverlay: some View { 1163 - ForEach(likeParticleBursts, id: \.self) { _ in 1164 - ForEach(0 ..< 5) { i in 1165 - LikeParticleView(index: i) 1166 - } 1167 - } 1168 - .scaleEffect(1.4) 1169 - } 1170 - 1171 1137 private func doubleTapLike(at point: CGPoint) { 1172 1138 hearts.append(HeartAnimationState(position: point)) 1173 - addLikeParticleBurst() 1139 + heartBeatTrigger &+= 1 1174 1140 guard !isFavorited, !isFavoriting else { return } 1175 1141 triggerFavoriteToggle() 1176 - } 1177 - 1178 - private func addLikeParticleBurst() { 1179 - let id = UUID() 1180 - likeParticleBursts.append(id) 1181 - DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) { 1182 - likeParticleBursts.removeAll { $0 == id } 1183 - } 1184 1142 } 1185 1143 1186 1144 private func triggerFavoriteToggle() {