-
Notifications
You must be signed in to change notification settings - Fork 0
Defer gallery video playback until the page transition ends #42
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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)") | ||
|
Comment on lines
+39
to
+50
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- VideoContainerView outline ---'
ast-grep outline swiftchan/Views/Media/Video/VideoContainerView.swift --view compact || true
printf '%s\n' '--- VideoContainerView lines 1-150 ---'
cat -n swiftchan/Views/Media/Video/VideoContainerView.swift | sed -n '1,150p'
printf '%s\n' '--- VerticalPagerView lines 1-90 ---'
cat -n swiftchan/Views/Media/Gallery/VerticalPagerView.swift | sed -n '1,90p'
printf '%s\n' '--- diff stat ---'
git diff --stat -- swiftchan/Views/Media/Video/VideoContainerView.swift swiftchan/Views/Media/Gallery/VerticalPagerView.swiftRepository: vanities/swiftchan Length of output: 11493 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- VerticalPagerView lines 84-260 ---'
cat -n swiftchan/Views/Media/Gallery/VerticalPagerView.swift | sed -n '84,260p'
printf '%s\n' '--- Remaining VideoContainerView.swift ---'
cat -n swiftchan/Views/Media/Video/VideoContainerView.swift | sed -n '131,320p'
printf '%s\n' '--- VideoContainerView call sites ---'
rg -n -C 8 'VideoContainerView\s*\(' swiftchan
printf '%s\n' '--- lifecycle references ---'
rg -n -C 4 'lifecycle|onChange\(of: isSelected\)|onDisappear' swiftchan/Views/Media/Video/VideoContainerView.swiftRepository: vanities/swiftchan Length of output: 17201 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- MediaView ---'
cat -n swiftchan/Views/Media/MediaView.swift | sed -n '1,130p'
printf '%s\n' '--- gallery references to MediaView/isSelected/VerticalPagerView ---'
rg -n -C 8 'VerticalPagerView|MediaView|isSelected' swiftchan/Views/Media swiftchan | sed -n '1,260p'
printf '%s\n' '--- focused source assertions and state model ---'
python3 - <<'PY'
from pathlib import Path
video = Path("swiftchan/Views/Media/Video/VideoContainerView.swift").read_text()
pager = Path("swiftchan/Views/Media/Gallery/VerticalPagerView.swift").read_text()
checks = {
"player is conditional on selection": "if let fileURL, isSelected" in video,
"callback is deferred": "DispatchQueue.main.async" in video,
"ready callback checks captured selection": "if isSelected, lifecycle.isActive, coordinator.playerLayer === playerLayer" in video,
"deselection pauses layer": "if !selected" in video and "coordinator.playerLayer?.pause()" in video,
"deselection updates lifecycle": "lifecycle.isActive = selected" in video,
"pager caches controllers": "controllers[index] = hosting" in pager,
"selection changes on completed transition": "parent.selection = hosting.index" in pager,
}
for name, result in checks.items():
print(f"{name}: {result}")
# Model the exact queued-callback race. The callback captured true when the
# player was created. The layer remains coordinator-owned until teardown.
captured_is_selected = True
lifecycle_is_active = True
same_layer = True
callback_would_play_before_fix = (
captured_is_selected and lifecycle_is_active and same_layer
)
# The proposed assignment executes before pause and before the queued callback.
lifecycle_is_active = False
callback_would_play_after_fix = (
captured_is_selected and lifecycle_is_active and same_layer
)
print(f"queued ready callback plays before fix: {callback_would_play_before_fix}")
print(f"queued ready callback plays after proposed fix: {callback_would_play_after_fix}")
PYRepository: vanities/swiftchan Length of output: 22653 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- VerticalPagerView usages ---'
rg -n -C 12 'VerticalPagerView' swiftchan
printf '%s\n' '--- isSelected assignments and selection callbacks ---'
rg -n -C 8 'isSelected\s*=|onPageChanged|selection\s*=' swiftchan/Views/Media swiftchan/Models swiftchan/ViewModels 2>/dev/null | sed -n '1,320p'
printf '%s\n' '--- gallery-related files ---'
fd -i 'gallery|media' swiftchan/Views swiftchan/ViewModels swiftchan/Models | sed -n '1,160p'Repository: vanities/swiftchan Length of output: 25964 🏁 Script executed: #!/bin/bash
set -eu
# Read-only final spot-check of the exact selection update path.
cat -n swiftchan/Views/Media/Gallery/GalleryView.swift | sed -n '37,86p;150,196p'Repository: vanities/swiftchan Length of output: 4413 Invalidate deferred playback callbacks on deselection. When the pager marks the previous media as unselected, its cached hosting controller can keep 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||
| 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 { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
Repository: vanities/swiftchan
Length of output: 13990
🏁 Script executed:
Repository: vanities/swiftchan
Length of output: 22042
🌐 Web query:
Apple UIPageViewController setViewControllers direction animated completion finished behavior overlapping transitions💡 Result:
The setViewControllers(_:direction:animated:completion:) method in UIPageViewController is used to programmatically set the currently displayed view controllers [1][2]. While straightforward in its intended use, developers frequently encounter specific behavioral issues—particularly with the scroll transition style—related to animation completion and state management. Behavioral Key Points: 1. Completion Block Reliability: The completion block provided to setViewControllers is expected to be called when the animation finishes [1][3]. However, it may not be called if the animation is interrupted, such as during device rotation or if another navigation call is initiated before the first completes [4][5][6][7]. In scenarios where an animation is canceled, the
finishedparameter in the completion block will typically be false [3]. 2. Overlapping Transitions and Caching: A known issue occurs when calling setViewControllers repeatedly or during an active transition, often leading to internal state inconsistencies or "overlapping" visual artifacts [4][5]. Specifically, when using UIPageViewControllerTransitionStyleScroll, the controller may cache the transition state [8][9]. If you force a jump to a non-adjacent page, the internal cache can become desynchronized from your data source, causing subsequent user-driven navigation (swiping) to fail or navigate to the wrong page [8][9]. 3. Mitigation Strategies: - Avoid Concurrent Calls: Ensure that a new call to setViewControllers is not initiated until the completion block of the previous call has finished [4]. - State Synchronization: If jumping to a non-adjacent page, some developers use a two-step approach: first jump to an adjacent neighbor without animation (animated: false), then perform the actual jump to the target page [9]. This forces the internal cache to update correctly [9]. - Rotation Handling: When handling rotations, avoid triggering setViewControllers during the transition process unless necessary. If you must, consider setting animated: false for the duration of the rotation to ensure the state remains predictable [5][6][7]. 4. Parameter Consistency: The view controllers passed to the method must represent the exact content visible after the animation completes [1]. If using UIPageViewControllerTransitionStylePageCurl with a double-sided spine, the array must contain both the front and back view controllers as required by the configuration [1].Citations:
🏁 Script executed:
Repository: vanities/swiftchan
Length of output: 300
Serialize in-flight programmatic transitions.
isSettingViewControlleris never read, so a second update can start before the first completion runs. An earlier completion can then notify a stale page. The ignored completionBoolalso prevents handling interrupted transitions.Coalesce pending selections, ignore stale completions, and reconcile
currentIndexwith the displayed controller only after a successful transition.🤖 Prompt for AI Agents
Source: MCP tools