Skip to content

Build the example app in the Swift 6 language mode - #1915

Merged
martinpucik merged 2 commits into
docs/manual-installationfrom
chore/example-matches-library
Aug 29, 2026
Merged

Build the example app in the Swift 6 language mode#1915
martinpucik merged 2 commits into
docs/manual-installationfrom
chore/example-matches-library

Conversation

@martinpucik

Copy link
Copy Markdown
Contributor

Stacked on #1914#1913#1911#1910#1909. Base is chore/remove-dead-files.

Summary

The package declares swiftLanguageModes: [.v6], but the example project still set SWIFT_VERSION = 5.0. CI built the example with no strict concurrency checking, so it validated none of the surface a consumer migrating to Swift 6 actually meets.

It found a real library defect

Three of MessageKit's delegate protocols carry @MainActor:

Protocol @MainActor before after
MessagesDataSource yes yes
MessagesLayoutDelegate yes yes
MessagesDisplayDelegate yes yes
MessageCellDelegate no yes
MessageLabelDelegate no yes

Both missing ones exist purely to handle taps inside cells, and the library only ever calls them from UIKit classes that are already main-actor isolated (MessageLabel, MessagesCollectionView, MessageContentCell, TextMessageCell). Every consumer building in Swift 6 had to work around this with @preconcurrency.

I annotated the protocols rather than papering over it in the example — a @preconcurrency there would have hidden the exact problem that building the example in Swift 6 is meant to surface.

The example app's own migration

  • AlertService and SampleData become @MainActor. One presents alerts, the other holds lazy var state that only view controllers read.
  • InputBarAccessoryView conformances marked @preconcurrency, matching the existing treatment of AVAudioPlayerDelegate in BasicAudioController. Those protocols carry no isolation of their own, so this is the right tool there — unlike MessageKit's own protocols, which I fixed at the source.
  • DispatchQueue.globalDispatchQueue.main hops become Task in the three example view controllers. They were passing non-Sendable arrays between queues for no benefit: the mock data generator is synchronous, and the hops existed only to fake latency, which Task.sleep expresses without crossing an isolation boundary.
  • ChatExampleTests becomes @MainActor, since it reads SampleData.

The deployment target is deliberately left at 15.0

I tried lowering it to 14.0 to match Package.swift. It does not build:

The iOS Simulator deployment target 'IPHONEOS_DEPLOYMENT_TARGET' is set to 14.0,
but the range of supported deployment target versions is 15.0 to 27.0.x.

Dependencies are not the blocker — Kingfisher 8.11.0 supports iOS 13 and InputBarAccessoryView 7.0.6 supports iOS 14. The current Xcode simply will not build an app below iOS 15.

This raises a question worth deciding separately: Package.swift advertises platforms: [.iOS(.v14)], but no app on a current toolchain can target iOS 14, so that claim cannot be exercised by the example or by CI. Whether to raise the package minimum to iOS 15 is a breaking change and your call — I have not touched it.

Verification

Builds clean for testing, zero errors. The only warnings are two pre-existing XCTest linkage notes about iOS 15 vs 17, unrelated to this change and present before it. make lint passes.

One cosmetic note

SwiftFormat's markTypes rule regenerates // MARK: headers from the conformance list, so @preconcurrency now leaks into four section comments, e.g. // MARK: @preconcurrency AttachmentManagerDelegate. That is the formatter's canonical output — hand-editing fails make lint. Suppressing it means changing the SwiftFormat config, which I left alone.

The package declares `swiftLanguageModes: [.v6]`, but the example app
project still set `SWIFT_VERSION = 5.0`. CI therefore built the example
without strict concurrency checking, so it validated none of the surface
that consumers migrating to Swift 6 actually meet.

Raising the example to Swift 6 found a defect in the library itself.
`MessagesDataSource`, `MessagesLayoutDelegate` and
`MessagesDisplayDelegate` all carry `@MainActor`, but `MessageCellDelegate`
and `MessageLabelDelegate` were missed, even though both exist to handle
taps inside cells and the library only ever calls them from UIKit classes
that are already main actor isolated. Annotate them, rather than papering
over the gap in the example, which would have hidden the very problem
that building the example in Swift 6 is meant to surface.

The rest is the example app's own migration:

- `AlertService` and `SampleData` become `@MainActor`. Both touch UIKit
  or hold mutable state that only view controllers read.
- Conformances to `InputBarAccessoryView` protocols are marked
  `@preconcurrency`, matching the existing treatment of
  `AVAudioPlayerDelegate` in `BasicAudioController`. Those protocols
  carry no isolation of their own.
- The `DispatchQueue.global` to `DispatchQueue.main` hops in the three
  example view controllers become `Task`. They passed non-Sendable
  arrays between queues for no reason: the mock data generator is
  synchronous, and the hops existed only to simulate latency, which
  `Task.sleep` expresses without crossing an isolation boundary.
- `ChatExampleTests` becomes `@MainActor`, since it reads `SampleData`.

The deployment target is left at 15.0. Lowering it to 14.0 to match the
package fails: the current Xcode supports deployment targets from 15.0
upward, so the example cannot be built against the iOS 14 minimum that
`Package.swift` declares.
`SWIFT_VERSION` is set once at the project level, so raising it to 6.0
also applies to `ChatExampleUITests`. XCUITest's API is main actor
isolated, so every `XCUIApplication` call in the test failed to compile.

Mark the test case `@MainActor`, matching `ChatExampleTests`.

This target is built by the `ChatExampleUITests` scheme, which `make
test_example` runs. The `ChatExample` scheme covers only the app and its
unit tests, so a build of that scheme alone does not reach this code.
@martinpucik

Copy link
Copy Markdown
Contributor Author

Pushed 0cae27d: the UI test target also needed @MainActor.

SWIFT_VERSION is set once at the project level, so raising it to 6.0 applied to ChatExampleUITests too, and XCUITest's API is main-actor isolated — every XCUIApplication call failed to compile.

My earlier "builds clean" claim was scoped too narrowly: I built the ChatExample scheme, which covers the app and its unit tests but not the UI test target. That lives in the separate ChatExampleUITests scheme, which make test_example runs. CI caught what my local check could not reach. Both schemes now build for testing locally.

Base automatically changed from chore/remove-dead-files to docs/manual-installation August 29, 2026 10:17
@martinpucik
martinpucik merged commit 4ef7d1d into docs/manual-installation Aug 29, 2026
6 of 7 checks passed
@martinpucik
martinpucik deleted the chore/example-matches-library branch August 29, 2026 10:17
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.

2 participants