Migrate Demo to ExyteChat 3.x and unblock the build on Xcode 27 - #448
Conversation
Demo/Demo.xcodeproj fails to link on Xcode 27 because ExyteChat's ChatView declares its @State properties in one file and its designated initializer in another, which trips a Swift 6.4/Xcode 27 compiler bug (swiftlang/swift#91700): the linker can't find the "variable initialization expression" symbol for each @State property's default value. - Bump exyte/Chat from 2.5.7 to 3.3.0, which required three follow-up fixes: - drop .messageUseMarkdown(true): ChatView 3.x always applies markdown attributes to message text now, the modifier no longer exists - drop Hashable from ResponsesStore.ConversationTurn: ExyteChat.Message lost Hashable in 3.x (Equatable only), and the conformance was unused - pin exyte/AnchoredPopup to >=1.2.2 explicitly: MediaPicker 3.4.x calls PopupParameters.displayMode(_:), added in AnchoredPopup 1.2.0, but MediaPicker's manifest still understates its own requirement as `from: "1.1.3"`, so SwiftPM could otherwise resolve an incompatible 1.1.3 and fail to build - Point Chat at a fork branch (nezhyborets/Chat@macpaw-xcode27-init-fix) that moves ChatView's designated init into the same file as its @State declarations, working around the Xcode 27 linker bug. Upstreamed as exyte/Chat#302. Revert to the tagged release once that lands and a fixed Xcode toolchain ships. Verified with a full clean build (not incremental) and by installing and launching Demo in the iOS Simulator. Fixes #444 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Pin the mutable fork dependency to an immutable commit or release tag.
Get a fresh assessment by requesting another Copilot review.
Review effort: Lite
Findings: 1
Open (1)
What changed in this PR
Migrates DemoChat to ExyteChat 3.x, updates incompatible APIs and dependency locks, and applies an Xcode 27 linker workaround.
Changes:
- Removes obsolete markdown configuration and
Hashableconformance. - Pins AnchoredPopup and refreshes resolved dependencies.
- Uses a forked Chat branch containing the compiler workaround.
Review note: Pin the fork to an immutable commit or release tag for reproducibility.
| File | Description |
|---|---|
Demo/DemoChat/Sources/UI/ResponsesChatDetailView.swift |
Removes obsolete markdown modifier. |
Demo/DemoChat/Sources/UI/DetailView.swift |
Removes obsolete markdown modifier. |
Demo/DemoChat/Sources/ResponsesStore.swift |
Removes incompatible Hashable conformance. |
Demo/DemoChat/Package.swift |
Updates Chat and AnchoredPopup dependencies. |
Demo/DemoChat/Package.resolved |
Refreshes Swift package versions. |
Demo/Demo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved |
Synchronizes Xcode dependency resolution. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
exyte/Chat#302 merged and shipped in 3.3.3, so the fork branch used as a stopgap is no longer needed. Point back at the upstream package. Verified with a full clean build of Demo on Xcode 27. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Update: exyte/Chat#302 merged and shipped in 3.3.3, so I switched this PR off the fork branch and back to the upstream package ( The fork branch ( |
Streaming requests cancelled the task as soon as a >=400 response came in, before the body was ever read, so OpenAIError.statusError only carried response headers and discarded OpenAI's actual error message (e.g. an invalid model name). Buffer the error body instead and decode it the same way the non-streaming path does, falling back to statusError only when the body isn't decodable. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
StreamingSessionErrorHandlingTests used HTTPURLResponse and URLSession.ResponseDisposition without the FoundationNetworking import that every other file touching these types already guards with, so Linux CI (Swift 5.10/6.0/6.3) failed to compile the test target. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Prompted by a real CI failure: a new test file used HTTPURLResponse / URLSession.ResponseDisposition without the FoundationNetworking import Linux needs, which compiled fine on macOS but broke all three Linux CI jobs. Add a standing reminder so future changes check for this. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Critical dependency and streaming error-handling issues remain, and the new test needs an explicit Foundation import.
Get a fresh assessment by requesting another Copilot review.
Review effort: Lite
Findings: 3
The response-disposition assertions captured and mutated a local var from inside the @sendable completionHandler closure, which Linux Swift 5.10's stricter checker rejects as a data race (macOS/newer toolchains only warn). Route the value through an instance array instead, matching how the other callback closures in this file already record their results. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
decodeErrorResponse indexed element 0 after decoding [GeminiAPIErrorResponse] without checking it was non-empty. A body that happens to decode as [] (e.g. literally "[]") traps with an index-out-of-range fatal error instead of falling through to the statusError fallback. This was reachable from the non-streaming path already, and became reachable from the streaming path too once it started decoding error bodies. Use .first instead of [0]. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CI takes several minutes per push; pushing after every small commit means paying that wait repeatedly. Commit locally as work progresses, but only push when explicitly asked to. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Once a >=400 response was accepted, every subsequent chunk was appended to errorData with no size limit and no cancellation path, so a malformed or malicious server/proxy could force unbounded memory growth by never ending the error body. Cap it at 256 KB (real API error bodies are a few hundred bytes) and cancel the task once exceeded; didCompleteWithError already falls back to statusError when the buffered body doesn't decode, so a truncated body degrades gracefully into the existing bounded-failure behavior. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Streaming error handling can exceed its size cap, and production callbacks are not guaranteed to execute in order.
Get a fresh assessment by requesting another Copilot review.
Review effort: Lite
Findings: 1


Summary
Closes #444.
Demo/Demo.xcodeprojwas failing to link on Xcode 27 with errors like:Root cause
Two separate issues stacked here:
ChatViewtrips a Swift 6.4/Xcode 27 compiler bug.ChatViewdeclares its@Stateproperties inChatView.swiftbut its designated initializer in a different file (ChatBuilderParameters.swift). That split-file shape is exactly what swiftlang/swift#91700 describes: the compiler can't link the "variable initialization expression" symbol for a@Stateproperty's default value when the initializer lives in another file. This affects ExyteChat 2.x (originally pinned here) and 3.x equally — it isn't a version issue.DemoChat.Changes
exyte/Chatfrom2.5.7to3.3.0and fix the fallout inDemoChat:.messageUseMarkdown(true)inDetailView.swift/ResponsesChatDetailView.swift— ChatView 3.x always applies markdown attributes to message text now, so the modifier no longer exists.HashablefromResponsesStore.ConversationTurn—ExyteChat.MessagelostHashablein 3.x (Equatableonly now), and the conformance wasn't used anywhere inDemoChat.exyte/AnchoredPopupto>=1.2.2explicitly.MediaPicker3.4.x callsPopupParameters.displayMode(_:), added inAnchoredPopup1.2.0, butMediaPicker's own manifest still understates its requirement asfrom: "1.1.3". Without an explicit pin, SwiftPM's resolver can legitimately land on 1.1.3 and fail with "value of type 'PopupParameters' has no member 'displayMode'".Chatdependency at a fork branch,nezhyborets/Chat@macpaw-xcode27-init-fix, which movesChatView's designated init into the same file as its@Statedeclarations — the workaround the upstream Swift issue describes. This is also open upstream as exyte/Chat#302; once that (or the underlying Apple compiler fix) ships,Package.swiftshould go back to.package(url: "https://github.com/exyte/Chat.git", from: "3.3.0")(comment left in place noting this).Test plan
xcodebuild -project Demo/Demo.xcodeproj -scheme Demo -destination "generic/platform=iOS Simulator" clean build— succeeds with zero errors (verified as a clean build, not incremental, since an incremental build can mask this specific linker failure by reusing a stale binary)