From 7f8a3bb9b5aadcc952680f800b7f5aa387af7b20 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Tue, 27 Oct 2026 23:36:43 +0100 Subject: [PATCH] Don't animate the video info carousel into place when it first opens On iPhone Duo the page first lays out at full width, then narrows for the status column, and the width-change re-snap animated from the first card to the tapped one. Only animate the re-snap when the current card was already on screen at the old width. --- Yattee/Views/Video/VideoInfoView.swift | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/Yattee/Views/Video/VideoInfoView.swift b/Yattee/Views/Video/VideoInfoView.swift index ab70db97..6c7a998e 100644 --- a/Yattee/Views/Video/VideoInfoView.swift +++ b/Yattee/Views/Video/VideoInfoView.swift @@ -41,6 +41,8 @@ struct VideoInfoView: View { #if os(iOS) @State private var carouselScrollPosition: Int? @State private var clearsCarouselPositionWhenIdle = false + /// Last scroll offset, kept in a box so tracking it doesn't re-render the view every frame + @State private var carouselOffset = CarouselOffsetBox() #endif // Continuation loading state @@ -1036,6 +1038,9 @@ struct VideoInfoView: View { .frame(width: cardWidth) .scrollTargetBehavior(.viewAligned(limitBehavior: .alwaysByOne)) .scrollPosition(id: $carouselScrollPosition) + .onScrollGeometryChange(for: CGFloat.self) { $0.contentOffset.x } action: { _, new in + carouselOffset.value = new + } .onScrollPhaseChange { oldPhase, newPhase in if newPhase == .interacting { clearsCarouselPositionWhenIdle = false @@ -1058,18 +1063,23 @@ struct VideoInfoView: View { proxy.scrollTo(newValue, anchor: .center) } } - .onChange(of: cardWidth) { _, _ in + .onChange(of: cardWidth) { oldW, _ 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. + // Only animate when the current card was on screen at the old width + // (unfold, rotation). The first layout of a page opened on a later + // card also arrives as a width change (iPhone Duo status column + // inset), and animating that scrolls visibly from the first card. guard let index = currentVideoIndex else { return } + let wasShowingCard = abs(carouselOffset.value - CGFloat(index) * (oldW + carouselSpacing)) < 1 carouselScrollPosition = nil DispatchQueue.main.async { clearsCarouselPositionWhenIdle = true - withAnimation(.linear(duration: 0.15)) { + withAnimation(wasShowingCard ? .linear(duration: 0.15) : nil) { carouselScrollPosition = index } } @@ -2873,3 +2883,9 @@ private struct VideoInfoScrollOffsetModifier: ViewModifier { .videoQueueContext(.init(video: .preview, queueSource: .manual, sourceLabel: "Manual", videoList: [.preview, .livePreview], videoIndex: 0, startTime: 0, loadMoreVideos: .none)) } } + +#if os(iOS) +private final class CarouselOffsetBox { + var value: CGFloat = 0 +} +#endif