diff --git a/ios/Platform/Platform/Features/Reader/ViewModels/ReaderViewModel.swift b/ios/Platform/Platform/Features/Reader/ViewModels/ReaderViewModel.swift index d496655..f25bca8 100644 --- a/ios/Platform/Platform/Features/Reader/ViewModels/ReaderViewModel.swift +++ b/ios/Platform/Platform/Features/Reader/ViewModels/ReaderViewModel.swift @@ -94,15 +94,19 @@ final class ReaderViewModel { func loadMore() async { guard !isLoadingMore, hasMore else { return } isLoadingMore = true + print("[SCROLL-DBG] 📥 loadMore START offset=\(offset)") do { let list = try await fetchEntries(offset: offset) + let count = list.entries.count entries.append(contentsOf: list.entries) total = list.total - offset += list.entries.count + offset += count hasMore = offset < list.total + print("[SCROLL-DBG] 📥 loadMore END appended=\(count) total=\(entries.count) hasMore=\(hasMore)") } catch { self.error = error.localizedDescription + print("[SCROLL-DBG] 📥 loadMore FAILED: \(error)") } isLoadingMore = false } diff --git a/ios/Platform/Platform/Features/Reader/Views/EntryListView.swift b/ios/Platform/Platform/Features/Reader/Views/EntryListView.swift index b8f5cc8..6763ab7 100644 --- a/ios/Platform/Platform/Features/Reader/Views/EntryListView.swift +++ b/ios/Platform/Platform/Features/Reader/Views/EntryListView.swift @@ -112,6 +112,7 @@ struct EntryListView: View { frame.maxY < 30 else { return } markedByScroll.insert(entryId) + print("[SCROLL-DBG] 📖 markRead id=\(entryId) entriesCount=\(vm.entries.count)") if let idx = vm.entries.firstIndex(where: { $0.id == entryId }) { vm.entries[idx].status = "read" diff --git a/ios/Platform/Platform/Features/Reader/Views/ScrollViewDriver.swift b/ios/Platform/Platform/Features/Reader/Views/ScrollViewDriver.swift index 52fd4be..b598b9d 100644 --- a/ios/Platform/Platform/Features/Reader/Views/ScrollViewDriver.swift +++ b/ios/Platform/Platform/Features/Reader/Views/ScrollViewDriver.swift @@ -94,25 +94,45 @@ struct ScrollViewDriver: UIViewRepresentable { displayLink = nil } + private var lastTickTime: CFAbsoluteTime = 0 + private var lastContentSize: CGFloat = 0 + private var tickCount = 0 + @objc private func tick(_ link: CADisplayLink) { guard let sv = scrollView else { stopAndNotify() return } + let now = CFAbsoluteTimeGetCurrent() let maxOffset = sv.contentSize.height - sv.bounds.height + sv.contentInset.bottom guard maxOffset > 0 else { return } - // 60pt/sec at 1.0x speed, scaled by actual frame duration - let delta = CGFloat(speed) * 60.0 * CGFloat(link.targetTimestamp - link.timestamp) - let newY = min(sv.contentOffset.y + delta, maxOffset) + let frameDuration = link.targetTimestamp - link.timestamp + let expectedDelta = CGFloat(speed) * 60.0 * CGFloat(frameDuration) + let beforeY = sv.contentOffset.y + let newY = min(beforeY + expectedDelta, maxOffset) sv.contentOffset.y = newY - // Notify delegate so tab bar minimize behavior triggers + let actualDelta = sv.contentOffset.y - beforeY + + // Detect jitter: contentSize changed, or actual delta differs from expected + let contentSizeChanged = sv.contentSize.height != lastContentSize + let wallDelta = lastTickTime > 0 ? (now - lastTickTime) * 1000 : 0 + let isJitter = contentSizeChanged || wallDelta > 25 // >25ms between frames = dropped frame + + tickCount += 1 + if isJitter || tickCount % 120 == 0 { + // Log on jitter or every 2 seconds + print("[SCROLL-DBG] \(isJitter ? "⚠️ JITTER" : "✅ ok") wallΔ=\(String(format:"%.1f", wallDelta))ms frameDur=\(String(format:"%.1f", frameDuration*1000))ms expΔ=\(String(format:"%.1f", expectedDelta))pt actΔ=\(String(format:"%.1f", actualDelta))pt y=\(Int(sv.contentOffset.y)) contentH=\(Int(sv.contentSize.height)) csChanged=\(contentSizeChanged) speed=\(String(format:"%.2f", speed))") + } + + lastTickTime = now + lastContentSize = sv.contentSize.height + originalDelegate?.scrollViewDidScroll?(sv) - // Stop at bottom if newY >= maxOffset - 1 { stopAndNotify() }