PlaybackManager: Incremental @MainActor adoption from the UI side β Part 2 - #4953
Conversation
Closes two more caller-side rings from the UI-side MainActor effort: - MainEpisodeActionView and EpisodeCell, finishing the type-level isolation Part 1 only started on MainEpisodeActionViewDelegate. - BookmarkListRouter, BookmarkListViewModel (and its two subclasses), and the four bookmark hosting controllers. UserEpisodeDetailProtocol is promoted from a per-method placeholder to full type-level @mainactor.
|
Claude finished @kean's task in 5m 4s ββ View job Review:
|
| Type | Already isolated via |
|---|---|
MainEpisodeActionView |
UIView |
EpisodeCell |
ThemeableSwipeCell β SwipeTableViewCell β UITableViewCell |
BookmarkDetailsViewController, BookmarkEpisodeListController, BookmarksProfileListController |
ThemedHostingController β ModifedHostingController β UIHostingController |
BookmarksPlayerTabController |
PlayerItemViewController β SimpleNotificationsViewController β UIViewController |
BookmarkListViewModel |
SearchableListViewModel β ListViewModel, explicitly @MainActor at ListViewModel.swift:11 |
Superclass isolation inference isn't affected by SWIFT_DEFAULT_ACTOR_ISOLATION = nonisolated β that default only applies where nothing else infers isolation, and a subclass can't diverge from its superclass's isolation anyway.
Harmless as documentation, and there's a reasonable argument for spelling it out. But it does change how the PR should be read: these rings were already closed, which is why "both rings closed cleanly β no latent off-main bugs surfaced" β nothing could surface from an annotation that doesn't change isolation. If the goal is documentation, the same sweep could drop the now-redundant @MainActor dynamic required init?(coder:) in the three hosting controllers and Task { @MainActor β¦ } at BookmarkListViewModel.swift:133-134.
2. EpisodeCell does have genuinely off-main @objc entry points
Pre-existing rather than introduced here, but this PR is the natural place to fix it since it's explicitly asserting the isolation. Three of the notifications the cell observes are posted with a plain NotificationCenter.default.post off the main thread:
userEpisodeUploadProgressβUploadProgressManager.swift:48, reached fromurlSession(_:task:didSendBodyData:β¦)(UploadManager+URLSessionDelegate.swift:78), i.e. the URLSession delegate queueuserEpisodeUploadStatusChangedβUploadManager+URLSessionDelegate.swift:63,RetrieveFileUploadStatus.swift:31/:43episodeTypeOrLengthChangedβMetadataTask.swift:71
The Thread.isMainThread fallbacks at EpisodeCell.swift:449 and :531 are there for exactly this. The issue is that both handlers touch self's state before hopping β uploadProgressDidUpdate reads episode, hits the DB via reloadEpisode(), and assigns self.episode (line 528) all off-main; updateCell reads episode?.uuid and hits the DB before its hop. Under @MainActor those accesses are now claimed to be serialized on the main actor, and with actor data-race checks enabled (Xcode Debug default) or a Swift 6 language-mode migration the @objc thunk traps on entry before the Thread.isMainThread branch is even reached. Details and a suggested nonisolated + hop-at-the-top rewrite are in the inline comment.
3. Minor
BookmarksPlayerTabController.willBeAddedToPlayer subscribes to playbackTrackChanged without .receive(on:), unlike the onBookmarkCreated sink directly below it. Safe today (that notification always goes through postOnMainThread) but it's relying on an unenforced convention now that the closure captures main-actor state. Inline comment has the detail, plus an unrelated note about cancellables accumulating across repeated willBeAddedToPlayer calls.
Not verified
I couldn't compile β this runner is Linux, no Xcode toolchain β so make build_staging / make test_staging weren't run. Everything above is from reading the source and the build settings; the "already isolated by inheritance" claim in particular is worth a quick sanity check on your machine (deleting one annotation should produce no diagnostics).
No security or performance concerns. Nothing here blocks the merge β the annotations are safe as written.
β’ branch kean/main-actor-playback-part3
Generated by π« Danger |
Marks the now-playing player screen and its supporting types (PlayerContainerViewController, NowPlayingPlayerItemViewController, ShowNotesPlayerItemViewController, PlayerTabsView, PlayerChapterCell, PlayerZoomAnimator, PlayerItemViewController base class) @mainactor, plus the PlayerItemContainerDelegate, PlayerTabDelegate, NowPlayingActionsDelegate, and TimeSliderDelegate protocols. ChaptersViewController and TranscriptViewController inherit isolation from the PlayerItemViewController base class. Also removes a redundant deinit nil-out of a weak delegate in ShowNotesPlayerItemViewController, and fixes a Sendable-closure warning on a DEBUG-only timer in TranscriptViewController.
|
Claude finished @kean's task in 5m 8s ββ View job Review:
|
|
Claude finished @kean's task in 3m 48s ββ View job Review:
|
|
Claude finished @kean's task in 1m 23s ββ View job Review:
|
Continues the incremental
@MainActoradoption started in #4947 (Part 1) and #4948 (Part 2). Independent of Part 2 β no overlapping files β so it targetstrunkdirectly rather than stacking.Closes three more caller-side rings:
MainEpisodeActionViewandEpisodeCellare now@MainActor, finishing the type-level isolation Part 1 only started onMainEpisodeActionViewDelegate.EpisodeCellis the main app's episode-row cell (Podcasts, Downloads, Starred, Uploaded, Listening History, Playlists).BookmarkListRouter,BookmarkListViewModel(and its two subclassesBookmarkEpisodeListViewModel/BookmarkPodcastListViewModel), and the four bookmark hosting controllers (BookmarkDetailsViewController,BookmarkEpisodeListController,BookmarksProfileListController,BookmarksPlayerTabController) are now@MainActor.PlayerItemViewController(base class),PlayerContainerViewController,NowPlayingPlayerItemViewController,ShowNotesPlayerItemViewController,PlayerTabsView,PlayerChapterCell, andPlayerZoomAnimatorare now@MainActor, along with thePlayerItemContainerDelegate,PlayerTabDelegate,NowPlayingActionsDelegate, andTimeSliderDelegateprotocols.ChaptersViewControllerandTranscriptViewControllerpick up isolation automatically via the base class.UserEpisodeDetailProtocolis promoted from a per-method placeholder to full type-level@MainActor.All rings closed cleanly. The player UI ring surfaced a couple of real fixes along the way:
BookmarkListRouterneeded its own@MainActoronce its conformers were isolated, andUserEpisodeDetailProtocol'sshowBookmarksdefault implementation needed explicit isolation since it lives in a protocol extension rather than a conforming type.AVRoutePickerViewDelegate,SFSafariViewControllerDelegate) and one widely-used app protocol (AnalyticsSourceProvider) aren't@MainActor-audited upstream, so those specific conformances use@preconcurrencyrather than promoting the shared protocols (AnalyticsSourceProvideralone has 24 unrelated conformers app-wide).ShowNotesPlayerItemViewController'sdeinitwas nil-ing out aweakdelegate β redundant, since ARC already zeroes weak references on deallocation, and not somethingdeinit(always non-isolated) can safely do to a@MainActorproperty anyway. Removed.TranscriptViewController'sTask.detachedtranscript loader now correctly hops back to@MainActorfor its UI-touching completion work (show(transcript:),show(error:),track(...)), which the compiler couldn't previously verify.To test
Checklist
CHANGELOG.mdif necessary.