Skip to content

Make the codebase clean under SWIFT_STRICT_CONCURRENCY=complete#1

Merged
tsvb merged 1 commit into
mainfrom
claude/flamboyant-gauss-89dd35
Jun 27, 2026
Merged

Make the codebase clean under SWIFT_STRICT_CONCURRENCY=complete#1
tsvb merged 1 commit into
mainfrom
claude/flamboyant-gauss-89dd35

Conversation

@tsvb

@tsvb tsvb commented Jun 27, 2026

Copy link
Copy Markdown
Owner

What

Resolves every data-race-checking warning surfaced by building with SWIFT_STRICT_CONCURRENCY=complete (still in the Swift 5 language mode), and turns the setting on permanently in project.yml.

Why

Complete concurrency checking is the prerequisite for eventually adopting the Swift 6 language mode. The codebase had ~11 pre-existing warnings blocking it. With them cleared and the flag enabled in the project base settings, any new concurrency issue now shows up as a warning immediately (and as an error once SWIFT_VERSION is raised to 6).

Changes

  • Theme.swiftTheme and its token types (TransportSignature, AccentMode, WashStyle, Capabilities, Palette, Typography, IdleCopy) now conform to Sendable. The actual blocker was Typography's (CGFloat) -> Font closures, now marked @Sendable. This also makes the four Theme statics and ThemeEnvironment.defaultValue concurrency-safe.
  • Catalog.swiftisoTimestamp is a computed property instead of a shared, non-Sendable ISO8601DateFormatter static. It's only hit a handful of times per livestream load, and the retry path right below already created a formatter per call.
  • PlayerService.swift / VideoPlayerService.swift — the KVO status/timeControlStatus observers re-capture [weak self] on the inner DispatchQueue.main.async closure and run their body inside MainActor.assumeIsolated. Previously the inner @Sendable closure closed over the outer closure's weak-self capture, which strict concurrency flags as "reference to captured var 'self' in concurrently-executing code".
  • PlayerService.swift — the didPlayToEndTime handler now derives a Sendable ObjectIdentifier and sends only that across the main-actor hop, instead of capturing the non-Sendable Notification / AVPlayerItem. itemDidEnd compares by identity.
  • project.yml — base settings set SWIFT_STRICT_CONCURRENCY: complete.

Note on the observer fix

I used MainActor.assumeIsolated + an inner [weak self] rather than Task { @MainActor in … }. A Task's pure-Swift @Sendable closure would have introduced new warnings for capturing the non-Sendable AVPlayerItem/AVPlayer, whereas keeping the DispatchQueue.main.async hop preserves the existing semantics and matches the idiom already used elsewhere in these files.

Verification

  • Full from-scratch build (fresh -derivedDataPath) under SWIFT_STRICT_CONCURRENCY=complete: 46 Swift files compiled, 0 errors, 0 warnings, BUILD SUCCEEDED.
  • Build under SWIFT_STRICT_CONCURRENCY=minimal: also clean — the non-strict build is not regressed.

The .xcodeproj is generated by xcodegen (git-ignored); run xcodegen generate after pulling to pick up the project.yml change.

🤖 Generated with Claude Code

Clears the data-race-checking warnings that blocked enabling complete
concurrency checking (a prerequisite for the Swift 6 language mode) and
turns the setting on in project.yml so any new issue surfaces immediately.

- Theme and its token types are now Sendable; Typography's font closures
  are marked @sendable (the one non-Sendable member that broke the chain),
  which also makes the Theme statics and ThemeEnvironment.defaultValue safe.
- Catalog.isoTimestamp is a computed property rather than a shared,
  non-Sendable ISO8601DateFormatter static.
- The KVO status/rate observers re-capture [weak self] on the inner
  DispatchQueue.main.async closure and run their body in
  MainActor.assumeIsolated, so self is no longer captured from the outer
  closure's weak var in concurrently-executing code.
- The didPlayToEndTime handler sends an ObjectIdentifier across the main
  hop instead of the non-Sendable Notification / AVPlayerItem.

Verified: a full from-scratch build under SWIFT_STRICT_CONCURRENCY=complete
is warning-free, and a SWIFT_STRICT_CONCURRENCY=minimal build still succeeds
(no regression to the non-strict build).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@tsvb tsvb left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Reviewed and verified — ready to merge. (Posting as a comment since GitHub blocks approving one's own PR.)

Independently confirmed the central claim: a fresh clean build in an isolated worktree with SWIFT_STRICT_CONCURRENCY=complete (fresh -derivedDataPath so incremental can't fake it) produced zero concurrency warnings, zero errors, BUILD SUCCEEDED.

Per file

  • Theme.swiftSendable on the token types plus @Sendable (CGFloat) -> Font on the Typography closures. The closure annotation is the load-bearing fix (Theme can't be Sendable until its closures are); the font-builders capture no mutable state, so it's sound.
  • Catalog.swiftisoTimestamp moved from static let to a computed property because ISO8601DateFormatter isn't Sendable. Correct. Minor, non-blocking: a formatter is now allocated per call (~N per livestream load); fine for that rare path, worth revisiting only if it ever gets hot.
  • project.ymlSWIFT_STRICT_CONCURRENCY: complete under Swift 5 mode: data-race issues surface as warnings now and become errors at the eventual Swift-6 flip. Right call.
  • PlayerService / VideoPlayerService observers — behaviorally identical; the changes are isolation hygiene. The inner DispatchQueue.main.async re-captures [weak self] (so @Sendable checking doesn't flag closing over the outer capture) and MainActor.assumeIsolated supplies the isolation — valid because the hop always lands on the main actor. The end-observer's ObjectIdentifier hop correctly avoids sending the non-Sendable Notification/AVPlayerItem across the boundary while preserving the exact identity match (itemID == ObjectIdentifier(currentItem)). No retain cycles introduced, no logic change.

Optional nits (non-blocking): the per-call formatter allocation above, and the async { [weak self] in assumeIsolated { … } } pattern now repeats across four observers — a small onMain { } helper could DRY it. Pure style.

The note about running xcodegen generate after pulling (the .xcodeproj is generated/git-ignored and is what picks up the project.yml setting) is exactly right and worth keeping prominent for anyone testing the branch.

@tsvb
tsvb merged commit de2716e into main Jun 27, 2026
1 check passed
@tsvb
tsvb deleted the claude/flamboyant-gauss-89dd35 branch June 28, 2026 20:28
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