diff --git a/swiftchan/Views/Media/Gallery/GalleryView.swift b/swiftchan/Views/Media/Gallery/GalleryView.swift index 18d98b7..53e49bb 100644 --- a/swiftchan/Views/Media/Gallery/GalleryView.swift +++ b/swiftchan/Views/Media/Gallery/GalleryView.swift @@ -68,8 +68,9 @@ struct GalleryView: View { .onChange(of: state.galleryIndex) { _, newValue in guard selection != newValue, viewModel.media.indices.contains(newValue) else { return } + // Only move the pager here. Activation (and video playback) is + // driven by onPageChanged, which fires when the transition ends. selection = newValue - updateActiveMedia(to: newValue) } .onChange(of: viewModel.media) { _, newMedia in guard newMedia.indices.contains(selection) else { diff --git a/swiftchan/Views/Media/Gallery/VerticalPagerView.swift b/swiftchan/Views/Media/Gallery/VerticalPagerView.swift index 5f5268f..202131f 100644 --- a/swiftchan/Views/Media/Gallery/VerticalPagerView.swift +++ b/swiftchan/Views/Media/Gallery/VerticalPagerView.swift @@ -50,10 +50,15 @@ struct VerticalPagerView: UIViewControllerRepresentable { let target = context.coordinator.controller(for: clampedSelection) else { return } let direction: UIPageViewController.NavigationDirection = clampedSelection >= context.coordinator.currentIndex ? .forward : .reverse + let animated = abs(clampedSelection - context.coordinator.currentIndex) == 1 context.coordinator.isSettingViewController = true - uiViewController.setViewControllers([target], direction: direction, animated: abs(clampedSelection - context.coordinator.currentIndex) == 1) { _ in + uiViewController.setViewControllers([target], direction: direction, animated: animated) { _ in context.coordinator.isSettingViewController = false context.coordinator.currentIndex = clampedSelection + // Report the change only once the programmatic transition has fully + // ended, so media activation (and therefore video playback) never + // begins mid-animation. + context.coordinator.notifyPageChanged(clampedSelection) } } } @@ -72,6 +77,10 @@ extension VerticalPagerView { super.init() } + func notifyPageChanged(_ index: Int) { + parent.onPageChanged?(index) + } + func update(parent: VerticalPagerView, controller: UIPageViewController) { self.parent = parent self.pageViewController = controller diff --git a/swiftchan/Views/Media/Video/VideoContainerView.swift b/swiftchan/Views/Media/Video/VideoContainerView.swift index b375c2f..e836c1e 100644 --- a/swiftchan/Views/Media/Video/VideoContainerView.swift +++ b/swiftchan/Views/Media/Video/VideoContainerView.swift @@ -36,11 +36,18 @@ struct VideoContainerView: View { var body: some View { ZStack { - if let fileURL { + // Only build the player once this page is the settled, active one. + // KSPlayerLayer autoplays on construction (KSOptions.isAutoPlay), and + // UIPageViewController instantiates the adjacent page as soon as the + // drag begins — constructing eagerly leaks the next video's audio + // mid-swipe. Gating on isSelected (set in didFinishAnimating) means no + // player exists until the page transition has fully ended. + if let fileURL, isSelected { KSVideoPlayer(coordinator: coordinator, url: fileURL, options: ksOptions()) .onStateChanged { playerLayer, state in // Defer state updates to avoid "Publishing changes from within view updates" DispatchQueue.main.async { + debugPrint("🎬 state=\(state) selected=\(isSelected) \(url.lastPathComponent)") switch state { case .readyToPlay: // Guard against resurrecting an orphaned player: this block can @@ -102,18 +109,12 @@ struct VideoContainerView: View { await loadVideo() } .onChange(of: isSelected) { _, selected in + debugPrint("🎬 isSelected=\(selected) \(url.lastPathComponent)") if !selected { + // Tearing down the KSVideoPlayer above dismantles the layer, but + // pause first so audio stops on the same runloop tick as the swipe. coordinator.playerLayer?.pause() isPlaying = false - } else if fileURL != nil { - // Debounce play to avoid triggering during drag - Task { - try? await Task.sleep(nanoseconds: 100_000_000) // 100ms - if isSelected, lifecycle.isActive { - coordinator.playerLayer?.play() - isPlaying = true - } - } } } .onAppear {