From 4d690d6b71353296f416eb999f923633fa678308 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Thu, 8 Oct 2026 23:59:53 +0200 Subject: [PATCH] Keep the video info carousel aligned when its width changes Unfolding an iPhone Duo kept the old scroll offset, leaving the selected card misaligned. Re-snap to the current card on width change, and clear the requested position once it settles so the lazy stack doesn't pull the next swipe back halfway between cards. --- Yattee/Views/Video/VideoInfoView.swift | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/Yattee/Views/Video/VideoInfoView.swift b/Yattee/Views/Video/VideoInfoView.swift index dc94b9bb..418883e2 100644 --- a/Yattee/Views/Video/VideoInfoView.swift +++ b/Yattee/Views/Video/VideoInfoView.swift @@ -37,6 +37,7 @@ struct VideoInfoView: View { // Carousel scroll state (iOS only) - separate from currentVideoIndex to avoid mid-scroll updates #if os(iOS) @State private var carouselScrollPosition: Int? + @State private var clearsCarouselPositionWhenIdle = false #endif // Continuation loading state @@ -1011,6 +1012,15 @@ struct VideoInfoView: View { .scrollTargetBehavior(.viewAligned(limitBehavior: .alwaysByOne)) .scrollPosition(id: $carouselScrollPosition) .onScrollPhaseChange { oldPhase, newPhase in + if newPhase == .interacting { + clearsCarouselPositionWhenIdle = false + } + // Drop the re-snap request after a width change (see onChange(of: cardWidth)) + if newPhase == .idle, clearsCarouselPositionWhenIdle { + clearsCarouselPositionWhenIdle = false + carouselScrollPosition = nil + return + } // Only update currentVideoIndex when scrolling ends (user lifts finger) if newPhase == .idle, let newIndex = carouselScrollPosition, newIndex != currentVideoIndex { currentVideoIndex = newIndex @@ -1023,6 +1033,22 @@ struct VideoInfoView: View { proxy.scrollTo(newValue, anchor: .center) } } + .onChange(of: cardWidth) { _, _ in + // A width change (iPhone Duo unfold, rotation, split resize) keeps + // the old point offset, leaving the cards misaligned. Re-snap to the + // current card (setting the same id is a no-op, hence the nil hop). + // The LazyHStack keeps re-applying a programmatic position while it + // realizes cards, which stopped the next swipe halfway between two + // cards, so the position is cleared again once the re-snap settles. + guard let index = currentVideoIndex else { return } + carouselScrollPosition = nil + DispatchQueue.main.async { + clearsCarouselPositionWhenIdle = true + withAnimation(.linear(duration: 0.15)) { + carouselScrollPosition = index + } + } + } .onAppear { // Initialize scroll position if carouselScrollPosition == nil, let index = currentVideoIndex {