Fix the media viewer getting stuck on "Loading more…" when swiping forward

QuickLook requests every page up front when the viewer opens. At that point the
media timeline is still empty, so every page after the tapped media is cached as
a forward pagination placeholder. Once the timeline loads it reports forward as
end reached, so no pagination happens and nothing ever replaces those cached
placeholders, leaving the viewer stuck when swiping forward.

When landing on a placeholder, or when the timeline updates, the controller now
checks whether the data source holds a different item at that index and if so
refreshes the current page. The refresh waits for any swipe to settle, as
refreshing mid-swipe breaks QLPreviewController.
This commit is contained in:
Mauro Romito
2026-09-30 17:17:32 +02:00
committed by Mauro
parent 61384f9986
commit 7911df9537
@@ -48,6 +48,14 @@ class TimelineMediaPreviewController: QLPreviewController {
captionHostingController.view captionHostingController.view
} }
/// Whether the displayed placeholder's index now holds a media, or a different placeholder
/// having reached the end of the timeline, in which case what's on display is stale.
private var isDisplayedPlaceholderStale: Bool {
guard let displayedItem = currentPreviewItem as? TimelineMediaPreviewItem.Loading else { return false }
let dataSource = context.viewState.dataSource
return dataSource.previewController(self, previewItemAt: currentPreviewItemIndex) as AnyObject !== displayedItem
}
override var overrideUserInterfaceStyle: UIUserInterfaceStyle { override var overrideUserInterfaceStyle: UIUserInterfaceStyle {
get { .dark } get { .dark }
set { } set { }
@@ -233,6 +241,12 @@ class TimelineMediaPreviewController: QLPreviewController {
if let previewItem = currentPreviewItem as? TimelineMediaPreviewItem.Media { if let previewItem = currentPreviewItem as? TimelineMediaPreviewItem.Media {
context.send(viewAction: .updateCurrentItem(.media(previewItem))) context.send(viewAction: .updateCurrentItem(.media(previewItem)))
} else if let loadingItem = currentPreviewItem as? TimelineMediaPreviewItem.Loading { } else if let loadingItem = currentPreviewItem as? TimelineMediaPreviewItem.Loading {
// QuickLook prefetches every item up front, so the placeholder may predate a timeline update.
guard !isDisplayedPlaceholderStale else {
Task { await refreshStalePlaceholder() }
return
}
switch loadingItem.state { switch loadingItem.state {
case .paginating: case .paginating:
context.send(viewAction: .updateCurrentItem(.loading(loadingItem))) context.send(viewAction: .updateCurrentItem(.loading(loadingItem)))
@@ -255,14 +269,18 @@ class TimelineMediaPreviewController: QLPreviewController {
} }
private func handleUpdatedItems() { private func handleUpdatedItems() {
guard let displayedItem = currentPreviewItem as? TimelineMediaPreviewItem.Loading else { return } guard isDisplayedPlaceholderStale else { return }
Task { await refreshStalePlaceholder() }
// The index may now hold a media, or a different placeholder having reached the end of }
// the timeline, in which case what's on display is stale.
let dataSource = context.viewState.dataSource private func refreshStalePlaceholder() async {
if dataSource.previewController(self, previewItemAt: currentPreviewItemIndex) as AnyObject !== displayedItem { // Refreshing whilst swiping completely breaks the QLPreviewController, so wait for the swipe to settle.
refreshCurrentPreviewItem() // This will trigger loadCurrentItem automatically. while let pageScrollView, pageScrollView.isDragging || pageScrollView.isDecelerating {
try? await Task.sleep(for: .seconds(0.1))
} }
guard isDisplayedPlaceholderStale else { return }
refreshCurrentPreviewItem() // This will trigger loadCurrentItem automatically.
} }
private func handleFileLoaded(itemID: MediaPreviewItemID) { private func handleFileLoaded(itemID: MediaPreviewItemID) {