Guidance for AI coding agents (Copilot, Cursor, Aider, Claude, etc.) working in this repository. Human readers are welcome, but this file is written for tools.
This repo hosts Stream's iOS Chat SDK in Swift. It provides:
- A low-level client (StreamChat) for the Stream Chat API — models, networking, state, persistence
- A UIKit-based UI SDK (StreamChatUI) that provides ready-made chat screens and components
- A shared UI module (StreamChatCommonUI) with appearance tokens, localization, and formatters used by both UIKit and SwiftUI SDKs
- SwiftUI AI components (StreamChatAI) for AI chat experiences: streaming markdown messages, reasoning, tool calls, charts, and a prompt composer. It depends on StreamCore (
StreamCoreandStreamCoreUI) but not on the other modules
Agents should prioritize backwards compatibility, API stability, and high test coverage when changing code. Avoid doing any source-breaking changes without adding deprecations.
- Language: Swift 6.0 (strict concurrency enabled —
swift-tools-version:6.0) - Primary distribution: Swift Package Manager (SPM)
- Project file:
StreamChat.xcodeproj(used for builds and tests; SPM manifest does not declare test targets) - Xcode: 16.x or newer (Apple Silicon supported)
- Platforms / deployment targets: iOS 13+, macOS 11+ (see
Package.swift; do not lower targets without approval) - CI: GitHub Actions + Fastlane (see
.github/workflows/smoke-checks.yml) - Linting: SwiftLint (v0.59.1) — config in
.swiftlint.yml - Formatting: SwiftFormat (v0.58.2) — config in
.swiftformat - Code generation: SwiftGen (v6.5.1) — generates
L10n.swiftfor localization strings - Git hooks: lefthook (
lefthook.yml) — runs SwiftLint fix + SwiftFormat on pre-commit, SwiftLint strict on pre-push - Tool versions are pinned in
Githubfile
- StreamCore from
stream-core-swift(exact 0.6.2) - swift-docc-plugin (exact 1.0.0) — for documentation generation
- Splash (exact 0.16.0) and swift-markdown-ui (exact 2.4.0) — used only by StreamChatAI
- MarkdownUI requires iOS 15 / macOS 12, above the package's iOS 13 / macOS 11 (kept on purpose for existing customers). Xcode 27+ raises the StreamChatAI target to match; Xcode 26 and older reject it. So StreamChatAI is not part of
Integration/SPM, whose CI lane runs on Xcode 26.2 - StreamChatAI must not depend on the Model Context Protocol SDK (it adds a large dependency tree);
AIClientToolDefinitionreads and writes the same JSON as MCP'sTool, with schemas as StreamCore'sRawJSON - Vendored libraries (do not edit directly):
Sources/StreamChatUI/StreamSwiftyGif/— vendored SwiftyGifSources/StreamChatUI/StreamDifferenceKit/— vendored DifferenceKit for collection diffingSources/StreamChat/StreamStarscream/— vendored Starscream for WebSocket- Update these via
make update_swiftygif version=X.Y.Z/make update_differencekit version=X.Y.Z
Sources/
StreamChat/ # Core client: API, models, networking, state, persistence
APIClient/ # REST API layer
Audio/ # Audio playback/recording utilities
Config/ # SDK configuration
Controllers/ # Channel, message, user controllers
Database/ # Core Data persistence (DTOs, model, migrations)
Extensions/ # Foundation/Swift extensions
Generated/ # Auto-generated (version) — do not edit manually
Models/ # Domain models (Channel, Message, User, etc.)
Query/ # Query types for filtering/sorting
Repositories/ # Data repositories
StateLayer/ # State observation layer
StreamStarscream/ # Vendored — do not edit
Utils/ # Utilities, common helpers
WebSocketClient/ # WebSocket connection management
Workers/ # Background workers (event handling, sync)
StreamChatUI/ # UIKit components: views, view controllers
ChatChannel/ # Channel view & sub-components
ChatChannelList/ # Channel list view controller
ChatMessageList/ # Message list rendering
ChatThread/ # Thread components
ChatThreadList/ # Thread list
CommonViews/ # Shared/reusable UIKit views
Composer/ # Message composer
Gallery/ # Media gallery
MessageActionsPopup/ # Message action menus
Navigation/ # Navigation routers
Resources/ # UIKit-specific resources
Utils/ # UIKit utilities
VoiceRecording/ # Voice recording components
StreamSwiftyGif/ # Vendored — do not edit
StreamDifferenceKit/ # Vendored — do not edit
StreamChatCommonUI/ # Shared appearance, localization, formatters
Appearance+*/ # Color palette, design tokens, fonts, images, formatters
Generated/ # Auto-generated (L10n.swift) — do not edit manually
Reactions/ # Reaction types and utilities
Resources/ # Localization files (en.lproj, etc.)
Utils/ # Common UI utilities
StreamChatAI/ # SwiftUI AI components (depends only on StreamCore)
AI/ # Non-UI code: what agents send, and how the device answers
Charts/ # Chart specs parsed from code blocks (Chart.js, ECharts, Vega-Lite, …)
LocalModel/ # On-device model fallback
MessageParts/ # A reply's ai_reasoning / ai_tool_call steps
ToolCalls/ # Client tools: definitions, registration, runner, results, approvals
UI/ # SwiftUI views and the state behind them
Appearance/ # AIAppearance: StreamCore design tokens, AI colors and images
Charts/ # Chart rendering
CommonViews/ # Sidebar
Composer/ # Prompt composer, view factory, suggestions, attachments
Message/ # Streaming Markdown message, code blocks, typing indicator
MessageParts/ # Reasoning and tool call steps, tool approvals
Reasoning/ # Streaming reasoning view
Transcription/ # Speech to text
Utils/ # Shared modifiers and formatting
Resources/ # Localization files (en.lproj)
Utils/ # Localization accessors, exported modules
DemoApp/ # Primary demo app (use to validate UI changes)
DemoAppPush/ # Push notification extension for the demo
DemoShare/ # Share extension for the demo
Examples/ # Clone apps: iMessage, Slack, Messenger, YouTube, EdgeCases
Integration/ # SPM & CocoaPods integration samples
TestTools/
StreamChatTestTools/ # Test mocks, fakes, fixtures, assertions
StreamChatTestMockServer/ # Mock server for E2E tests
Tests/
StreamChatTests/ # Unit tests for StreamChat (mirrors source structure)
StreamChatUITests/ # Snapshot & unit tests for StreamChatUI
StreamChatCommonUITests/ # Tests for StreamChatCommonUI
StreamChatAITests/ # Tests for StreamChatAI
StreamChatUITestsApp/ # Test harness app for E2E tests
StreamChatUITestsAppUITests/ # E2E / UI automation tests
Scripts/ # Helper scripts (bootstrap, dependency updates)
fastlane/ # Fastlane lanes for CI (build, test, release)
Documentation.docc # DocC documentation catalog
When creating new source or resource files, add them to the correct Xcode target(s). Update the project (e.g. project.pbxproj) so each new file is included in the appropriate target's "Compile Sources" (or "Copy Bundle Resources" for assets). Match the target(s) used by sibling files in the same directory (e.g. Sources/StreamChat/ → StreamChat target; Sources/StreamChatUI/ → StreamChatUI; Sources/StreamChatCommonUI/ → StreamChatCommonUI; Sources/StreamChatAI/ → StreamChatAI; Tests/StreamChatTests/ → StreamChatTests target). Omitting target membership will cause build failures or unused files.
- Open the repository in Xcode (root contains
Package.swiftandStreamChat.xcodeproj). - Resolve packages.
- Choose an iOS Simulator (e.g., iPhone 17 Pro) and Build.
Optional: run Scripts/bootstrap.sh to install pinned versions of SwiftLint, SwiftFormat, and SwiftGen, and to set up lefthook git hooks.
The DemoApp target is a fully functional sample app. Prefer running it to validate UI changes. Keep demo configs free of credentials and use placeholders like YOUR_STREAM_KEY.
Available shared schemes (under StreamChat.xcodeproj/xcshareddata/xcschemes/):
StreamChat— builds the core frameworkStreamChatUI— builds the UIKit frameworkStreamChatCommonUI— builds the shared UI frameworkStreamChatAI— builds the AI components framework and runs its testsStreamChatTests— runs core unit testsStreamChatTestToolsTests— runs test tools testsDemoApp— builds and runs the demo appStreamChatUITestsApp— builds and runs the E2E test harnessStreamChatUITestsAppUITests— runs E2E / UI automation testsiMessage,Slack,Messenger,YouTube— example/clone apps
Agents must query existing schemes before invoking xcodebuild.
Prefer Xcode for day-to-day work; use CLI for CI parity & automation.
Build (Debug):
xcodebuild \
-project StreamChat.xcodeproj \
-scheme StreamChat \
-destination 'platform=iOS Simulator,name=iPhone 17 Pro' \
-configuration Debug build
Run tests:
xcodebuild \
-project StreamChat.xcodeproj \
-scheme StreamChatTests \
-destination 'platform=iOS Simulator,name=iPhone 17 Pro' \
-configuration Debug test
xcodebuild \
-project StreamChat.xcodeproj \
-scheme StreamChatUI \
-destination 'platform=iOS Simulator,name=iPhone 17 Pro' \
-configuration Debug test
If the device is not available, use one of the active booted devices, like so:
xcodebuild \
-project StreamChat.xcodeproj \
-scheme StreamChatTests \
-destination "platform=iOS Simulator,OS=any,name=$(xcrun simctl list devices booted | grep '(Booted)' | head -1 | sed 's/ (.*)//')" \
-configuration Debug test
SwiftLint (strict mode):
swiftlint lint --config .swiftlint.yml --strict
SwiftFormat (check only — no edits):
swiftformat --config .swiftformat --lint .
SwiftFormat (auto-fix):
swiftformat --config .swiftformat .
Respect .swiftlint.yml and .swiftformat rules. Do not broadly disable rules; scope exceptions and justify in PRs.
CI is driven by Fastlane (see fastlane/Fastfile). Key lanes:
test— runs StreamChat unit teststest_ui— runs StreamChatUI snapshot/unit teststest_common_ui— runs StreamChatCommonUI teststest_ai— runs StreamChatAI teststest_e2e_mock— runs E2E tests against a mock serverbuild_demo— builds the demo appbuild_test_app_and_frameworks— builds test app and SDK frameworksbuild_imessage_clone,build_slack_clone,build_messenger_clone,build_youtube_clone— builds example appsspm_integration— validates SPM integrationrun_swift_format— runs SwiftFormat validationvalidate_public_interface— checks for unintended public API changes
The smoke-checks.yml workflow is the primary PR gate. It runs linting, formatting validation, public interface checks, unit tests, E2E tests, demo app builds, and example app builds.
Do not manually edit files in Sources/StreamChat/Generated/:
SystemEnvironment+Version.swift— updated automatically during releases.OpenAPI/— produced byScripts/openapi_generate.sh. Regenerate instead of editing (see below).
Do not manually edit files in Sources/StreamChatCommonUI/Generated/:
L10n.swift— generated by SwiftGen from localization.stringsfiles. Edit the.stringssource files instead.L10n_template.stencil— the SwiftGen template for localization generation.
Scripts/openapi_generate.sh regenerates Sources/StreamChat/Generated/OpenAPI/ from the backend spec. The config (allow-lists) and the ordered pipeline steps live in that script; the transform functions live in Scripts/openapi_generate_helpers.sh (alphabetical).
Running it:
- Requires the
chatbackend repo checked out at../chat(the script runsmake openapiand builds the generator there), plusswiftformat,sourcery, andpython3. - Run
Scripts/openapi_generate.shfrom the repo root. It deletes and rebuilds the output directory and ends by printing the endpoints the spec offers butallowed_endpointsprunes. - If unrelated files change because the spec moved on, revert them with
git checkout HEAD -- <file>.
Adding an endpoint:
- Add it to
allowed_endpoints, but only if production code calls it. The "Unused endpoints" report lists the candidate names. - Add every model the endpoint reaches (the full transitive closure) to
allowed_models, using the generator's original names. A missing model surfaces as a build failure. - Rename new models onto the SDK names they replace with
rename_generated(the model file and its references) orrename_generated_type(references only). Point unused responses atEmptyResponse. - Classify every retained model, under its post-rename name, into exactly one of
encodable_only_models,decodable_only_models, orcodable_models. The script fails on any model that is unclassified or classified twice. Add the model toallowed_hashable_modelsonly if it needsHashable. - Add any new WebSocket events to
allowed_events. - Drop properties the SDK never reads with
remove_property, in the "Unread by the SDK" block. - To expose a model publicly, use
publicize_modelorpublicize_raw_representable.
Rules:
- Keep every list and every block of calls alphabetical. The only exception is the commented order-sensitive renames.
- Steps before the rename step (step 4) use the generator's original names; steps after it use the renamed names.
- Workarounds for backend spec bugs need a CHA ticket comment, e.g.
# Remove when fixed: CHA-XXXX. - Verify by regenerating, building the
StreamChatscheme, running the unit tests, and reviewinggit diff Sources/StreamChat/Generated/OpenAPI.
The SDK uses defaultLocalization: "en". String resources live in Sources/StreamChatCommonUI/Resources/. After modifying .strings files, regenerate L10n.swift by running SwiftGen (or let CI handle it). Always use L10n accessors for user-facing strings rather than raw string literals.
StreamChatAI keeps its own strings in Sources/StreamChatAI/Resources/en.lproj/Localizable.strings, read through the internal L10n in Sources/StreamChatAI/Utils/Localization.swift. That file is written by hand: add an accessor there for each new key. L10n reads every string through AIAppearance.localizationProvider, so apps can override them.
The project uses Swift 6.0 strict concurrency. When adding new code:
- Use
Sendableconformances where needed for cross-isolation transfers - Avoid introducing data races; the compiler will enforce actor isolation
- Mark UI-bound types and controllers appropriately for
@MainActorwhen necessary
Code documentation
- Write doc comments (
///) only forpublicdeclarations — types, methods, and properties that are part of the SDK's public API. - Do not add doc comments to
internal,private, or test code. - Keep doc comments concise: a one-line summary; add parameter/return docs only when they are not obvious from the signature.
- Do not add inline comments narrating what the code or a change does; comment only non-obvious constraints or reasoning.
Accessibility & UI quality
- Ensure UIKit components have accessibility labels, traits, and dynamic type support.
- Support both light/dark mode.
- Use the Appearance system (
Appearance,Components) for theming and configuration. - In StreamChatAI, keep code without UI (models, parsing, tool calls) under
AI/, importing Foundation rather than SwiftUI, and views underUI/, one view per file. Split a view whose body grows long into smaller views. - In StreamChatAI, take colors, fonts, spacing and icons from
AIAppearance, never hard-coded values. Each view injects only what it reads, e.g.@Injected(\.aiAppearance.colors) var colors,@Injected(\.aiAppearance.fonts) var fonts,@Injected(\.aiAppearance.tokens.layout) var layout,@Injected(\.aiAppearance.images) var images. A new color or font goes inAIAppearance+Colors.swiftorAIAppearance+Fonts.swift, derived from the StreamCoreDesignSystemTokens; a new icon goes inAIAppearance.Images. Don't addInjectedValueskeys such as\.colorsor\.fonts: StreamChatSwiftUI already defines them. Log with StreamCore'slog, notprint. Keep re-exporting onlyDesignSystemTokensfrom StreamCoreUI: the rest of it clashes with StreamChatCommonUI names such asImageResizeandBoxShadow.
Testing policy
- Add/extend tests in the matching module's
Tests/folder (mirrors the source directory structure):Tests/StreamChatTests/for core client testsTests/StreamChatUITests/for UIKit component tests (including snapshot tests)Tests/StreamChatCommonUITests/for shared UI module testsTests/StreamChatAITests/for AI components tests
- Test infrastructure (mocks, shared helpers, fixtures) lives in
TestTools/StreamChatTestTools/ - Use fakes/mocks from the test helpers provided by the repo when possible.
- Use
waitForfrom StreamChatTestTools to await async completion handlers instead of manualXCTestExpectation+waitForExpectations. - Assert database state with
readSynchronously/writeSynchronouslyonDatabaseContainer— the same APIs the SDK uses — rather than reading directly fromviewContext.
Database conventions
- Core Data model lives in
Sources/StreamChat/Database/StreamChatModel.xcdatamodeld - In database DTOs under
Sources/StreamChat/Database/DTOs/, useDBDateinstead ofDate/NSDatefor NSManaged properties (enforced by SwiftLint custom rule) - Prefer
.pin()over.constraint()inSources/StreamChatUI/for Auto Layout (enforced by SwiftLint custom rule) - When a database write needs to produce a value for later use, prefer
write(converting:)over plainwritewith a capturednonisolated(unsafe) var.
Security & credentials
- Never commit API keys or customer data.
- Example code must use obvious placeholders (e.g.,
YOUR_STREAM_KEY). - If you add scripts, ensure they fail closed on missing env vars.
Compatibility & distribution
- Maintain compatibility with supported iOS versions listed in
Package.swift. - Don't introduce third-party deps without discussion.
- Validate SPM integration when changing module boundaries.
- The default integration branch is
develop. Feature branches are merged intodevelop. - Name branches with a type prefix and a short kebab-case description, e.g.
add/dynamic-search-debounce,fix/empty-search-results,docs/channel-search. - Prefer prefixes that match the change type:
add/,fix/,docs/, etc. Do not use personal name prefixes (e.g.nuno/...) or ticket IDs (e.g.ios-1932,IOS-1932) in branch names. - Update
CHANGELOG.mdunder the# Upcomingsection when making client-facing changes (follow the Keep a Changelog format with### Added,### Fixed,### Changedsubsections). - The changelog has separate subsections for StreamChat, StreamChatUI, StreamChatCommonUI, and StreamChatAI.
- Only update
CHANGELOG.mdafter the PR has been opened, so the entry can include the PR link (e.g.[#1234](https://github.com/GetStream/stream-chat-swift/pull/1234)). Push the changelog update as a follow-up commit on the same branch. - Keep changelog entries as short and high-level as possible. Describe the user-visible outcome in one line and do not explain implementation details, file names, or internal APIs.
- Start commit subject lines with a capital letter.
- Do not end commit subject lines with a period.
- Keep the subject line concise (ideally under 72 characters); put additional context in the body separated by a blank line.
- Do not include ticket IDs or Linear issue keys in commit subjects (e.g. avoid
[IOS-1932]prefixes). Link tickets from the PR instead.
- Use the Github CLI to create a PR and use the Linear MCP to link the relevant issue assigned to me.
- When creating a PR, the base branch should be the
developbranch. - Make sure that the PR respects the PR template in
.github/PULL_REQUEST_TEMPLATE.md. - Make sure to fill the template with atomic information, do not mention things that were done and then reverted in this same PR.
- Do not write "Made with Cursor" in the PR description.