Skip to content

Show chapters and bookmarks beside the player on iPhone Duo - #1640

Open
GianniCarlo wants to merge 4 commits into
developfrom
feat/duo-chapters-split
Open

GianniCarlo wants to merge 4 commits into
developfrom
feat/duo-chapters-split

Conversation

@GianniCarlo

@GianniCarlo GianniCarlo commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

The open iPhone Duo (951 × 669 pt in landscape) gives the player a screen it doesn't use: the one-column player sits in the middle of a wide display, and the chapters and bookmarks are still sheets that cover it.

Changes

  • PlayerListPanel (new): on iOS 27.1 the player is the primary side of an ArrangementView (.split, horizontal only), and the secondary side is a panel with the chapters and bookmarks, switched with a Chapters | Bookmarks segmented control in its navigation bar. It starts on the list Player Controls › Opens picks. Picking a chapter or bookmark jumps and leaves the panel on screen; the player closes from its grabber, as before.
    • ArrangementView decides when there's room. Measured on the Duo simulator: closed, or open in portrait, the player is alone and looks as it does today; open in landscape the two sides share the width (433.5 pt each, flat), and half-folded the split moves onto the fold.
    • Applied with .playerListPanel(...) after the player's button row, so the drag to close and the rounded background cover both sides. Below iOS 27.1 the player is unchanged.
    • The player's list button and More's Chapters/Bookmarks entry stay, even while the panel shows. SwiftUI can't tell whether ArrangementView shows its secondary side, and the closest signal, splitArrangementAxis, reads horizontal on a closed Duo in landscape too, where there's no panel.
  • ChaptersView / BookmarksView: no longer know where they're shown. They drop their own NavigationStack, close button and dismiss; whoever shows them provides those. PlayerView's PlayerListSheet does it for the sheets (navigation stack, close button, onSelect closes the sheet after a pick), and the panel has its own navigation stack. Reload and share stay in the lists' toolbars, so they show in both.
  • ChaptersViewModel: follows currentItemPublisher() and the item's $currentChapter, so the checkmark moves with playback and the rows change with the book. The list doesn't scroll when the chapter changes, so it never pulls away from chapters someone is browsing; it scrolls to the playing chapter when it opens, as before. This applies to the sheet too.
  • BookmarksViewModel: reloads after any Core Data save that touches a Bookmark (didSaveObjectIDsNotification, object IDs only), so a bookmark made with the player's button, an automatic bookmark or a sync shows up in the panel while it's on screen.
  • CI: Xcode 27.1 (the runner already has it); ArrangementView is in the iOS 27.1 SDK only. CLAUDE.md updated.

Testing

  • iPhone Duo simulator (iOS 27.1), by hand:
    • Open, landscape: player and panel side by side; the panel's bar has the Chapters | Bookmarks switch with Reload on chapters and share on bookmarks; the list button stays in the player's row.
    • The checkmark follows playback; tapping a chapter jumps with both sides staying open; a bookmark made with the player's button appears in the panel right away.
    • Half-folded (book pose) the split sits on the fold; portrait and closed (in landscape too) show the player alone with its list button; dragging the player down closes both sides.
  • iPhone 17 Pro simulator (iOS 27.0): the Chapters and Bookmarks sheets keep their close button on the leading side and Reload / share on the trailing side; picking a chapter closes the sheet and jumps; More opens Bookmarks; the chapters checkmark moves while the sheet is open.
  • iOS 26.5 simulator: the release check's testFreshLaunch, testImport and testPlayback pass (the player opens and plays).
  • Unit tests (all 957 pass): ChaptersViewModelTests stub the player's item publisher and gain two tests, for the checkmark following the chapter and the rows following the book. New BookmarksViewModelTests: a save touching a bookmark reloads the list, a save touching other entities doesn't.

Not covered: ArrangementView on other iPhones and iPad. The iOS 27.1 simulator runtime only runs iPhone Duo, so how it lays out there is to check once 27.1 is out of beta.

Before the next release: Xcode Cloud's Xcode version (App Store Connect) needs to be 27.1, or the release build won't compile.

… Duo

On iOS 27.1 the player is the primary side of an ArrangementView whose
secondary side is a panel with the chapters and bookmarks, switched with a
segmented control. ArrangementView shows it when there's room: the open
iPhone Duo in landscape, split on the fold when half-folded. Closed, or open
in portrait, the player is alone as before.

In the panel the lists have no close button and picking a row doesn't
dismiss. The chapters checkmark follows playback and the bookmarks reload
after any save that touches one, so both stay current while on screen. The
player's list button hides while the panel shows.

CI moves to Xcode 27.1, which has the iOS 27.1 SDK.
Comment thread BookPlayer/Player/Views/MediaActionRow.swift Outdated
@github-actions

github-actions Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

🟡 Claude PR Review — WARN

On iOS 27.1, the player now sits in an ArrangementView with a Chapters / Bookmarks panel beside it. ChaptersView and BookmarksView no longer build their own NavigationStack or close button: PlayerListSheet adds those for the sheets, and the panel has its own stack. ChaptersViewModel now follows currentItemPublisher() and the item's $currentChapter. BookmarksViewModel reloads when it sees a didSaveObjectIDsNotification that touches a Bookmark.

Concurrency / threading: Both new subscriptions are stored (named cancellables cancelled before rebind, or disposeBag), use [weak self], and hop to main with .receive(on: .main) before changing @Published state. The save observer only reads NSManagedObjectID.entity off the main thread, which is safe. It then re-fetches through getBookmarks on the view context, on main.

CoreData: Only Simple* / Playable* snapshots cross into the UI, and there's no model change.

Player / AVAudioSession, auth / entitlement: Not touched.

Still open: The build now needs the Xcode 27.1 SDK, with no compile-time fallback (finding 1). The earlier note about a sheet covering the panel (finding 2) was written against code that hid the list button; that button now stays by design, so I'm not repeating it.

Findings: 1 warn

Previously raised

Finding Status
BookPlayer/Player/Views/PlayerView.swift:142 (info) ⚠︎ moved 🟡 still open

Model claude-opus-5-5 · run log · 0 new · 1 carried over · 1 re-worded on their own thread · 0 resolved · advisory (a human should still review). Findings are de-duplicated across pushes; an earlier finding closes only when the verification pass judges it against the current code — fixed, no longer applicable, accepted by a maintainer, or a duplicate of a finding reported on this push.

- More no longer offers the chapters or bookmarks while the panel shows
  them. PlayerView reads whether the panel shows once and passes it to the
  button row and the More menu.
- The chapters list scrolls to the new chapter only while the playing
  chapter's row is on screen, so it doesn't jump away from chapters someone
  is browsing.
- BookmarksViewModelTests cover the reload: a save touching a bookmark
  reloads the list, a save touching other entities doesn't.
@GianniCarlo

Copy link
Copy Markdown
Collaborator Author

On the findings without an inline thread:

  • Bookmark reload test and auto-scroll: addressed in 574d13b. BookmarksViewModelTests covers a save touching a bookmark (reloads) and one touching other entities (doesn't). The chapters list now scrolls to the new chapter only while the playing chapter's row is on screen.
  • Bookmark.fetchRequest().entityName: keeping it. It compiles (CI passed on 14a2550 with Xcode 27.1), and Bookmark.entity().name returns nil in this app: with it, the filter never matched and the panel didn't refresh on the Duo simulator. The fetch request is built once, when the view model starts observing.
  • Segmented control label: keeping it unlabeled. VoiceOver reads each segment with its position ("Chapters, 1 of 2, selected"), and the only existing key that names the control is "Opens", which would be misleading here.

/// the screen's full width
buttonSpacing: isSideBySide ? 24 : nil,
/// The panel beside the player already shows the lists this button opens
showsListButton: !isPanelShown,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 INFO — The list button and More entry now hide while the panel shows. But a Chapters or Bookmarks sheet opened before the panel appeared stays up and covers both sides. For example: open Chapters in portrait on an open Duo, then rotate to landscape. Consider calling viewModel.hideSheet() when the panel appears and sheetStyle is .chapters or .bookmark, e.g. with an .onChange(of: isPanelShown) inside the PlayerListPanelReader closure.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving as is: the sheet still closes normally, and portrait on the open Duo gets its own layout in a follow-up PR.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reported again on the newest commit, worded differently — the current wording is:

The list button and the More entry now hide while the panel shows. But a Chapters or Bookmarks sheet opened before the panel appeared stays up and covers both sides. Example: open Chapters in portrait on an open Duo, then rotate to landscape. Consider calling viewModel.hideSheet() when the panel appears and sheetStyle is .chapters or .bookmark, e.g. with an .onChange(of: isPanelShown) inside the PlayerListPanelReader closure.

Following playback pulled the list away from chapters someone was
browsing. Tracking which rows are on screen to avoid that wasn't reliable
(the list drops some "row left the screen" events during a fling), and
the visibility modifier also kept the rows' background from reaching the
list. The list scrolls to the playing chapter when it opens, as before;
the checkmark still moves with playback.
@GianniCarlo

Copy link
Copy Markdown
Collaborator Author

On the remaining notes:

  • Visible-chapter tracking (ChaptersView): no longer applies. A set of visible rows (as suggested) jumped back to the playing chapter after flings in 3 of 4 tries on the simulator, because the list drops some "row left the screen" events while it recycles rows. The scroll on chapter change is gone in 979048f: the list scrolls to the playing chapter when it opens, and the checkmark still follows playback.
  • Other iPhones and iPad: known; it's in the description's "Not covered" and gets checked once iOS 27.1 is out of beta.

Comment thread .github/workflows/ci.yml
@@ -13,10 +13,10 @@ jobs:
timeout-minutes: 30
steps:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 WARN — This makes Xcode 27.1 a hard requirement for develop: ArrangementView exists only in the iOS 27.1 SDK, and #available doesn't help at compile time. The PR says 27.1 is still in beta ("once 27.1 is out of beta"), and App Store Connect rejects builds made with a beta Xcode. Once this merges, no release can be cut from develop until 27.1 ships and Xcode Cloud is switched to it.

Options:

  • hold the merge until 27.1 is final, or
  • guard PlayerListPanelModifier / SplitAxisReader with #if compiler(>=…) or an SDK check, so the code still builds with 27.0 and only turns the panel on with 27.1.

Also add this to the release checklist, not only the PR description.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reported again on the newest commit, worded differently — the current wording is:

This still makes Xcode 27.1 a hard requirement for develop. PlayerListPanelModifier uses ArrangementView / .arrangementViewStyle(.split…) behind #available(iOS 27.1, *) only. That is a runtime check, so Xcode 27.0 can't compile the file. If 27.1 is still in beta, App Store Connect won't accept builds from it, and no release can be cut from develop until 27.1 ships and Xcode Cloud moves to it.

Options:

  • hold the merge until 27.1 is final, or
  • wrap the ArrangementView branch of PlayerListPanelModifier.body in a compile-time check (#if compiler(>=6.x) for the 27.1 toolchain, or an SDK-version flag) so 27.0 builds still work and just show the player alone.

Also add the Xcode Cloud switch to the release checklist, not only to CLAUDE.md and the PR description.

…their presenters

- Drop the panel detection (PlayerListPanelReader, the list button and More entry
  hidden while the panel shows). SwiftUI can't tell whether ArrangementView shows its
  secondary side, and splitArrangementAxis reads horizontal on a closed Duo in landscape
  too, which hid the list button with no panel on screen. Both always show now.
- ChaptersView and BookmarksView no longer know where they're shown: no isBesidePlayer,
  no NavigationStack, close button or dismiss of their own. PlayerView's PlayerListSheet
  wraps them for the sheets (navigation stack, close button, onSelect closes the sheet
  after a pick); the panel keeps its own navigation stack with the Chapters | Bookmarks
  switch, and a pick there leaves it on screen.

This branch was successfully deployed

1 active deployment
reviewer — d5b69861 Deployed Oct 13, 2026 by GianniCarlo via review #258
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant