Repo:
element-hq/element-x-ios— iOS Matrix client (SwiftUI +matrix-rust-sdk). Keep current: Change break fact here — path, command, convention, structure? Fix that fact, same PR. Change not described here? Leave file alone. Edit file? Use caveman skill — match terse voice. No caveman, no edit.
PRs must follow rules. Prefer Xcode MCP tools over terminal commands.
- SwiftLint (.swiftlint.yml) + SwiftFormat (.swiftformat) enforce style on build. Warnings show in Xcode. Build before XcodeRefreshCodeIssuesInFile.
- Follow Swift API Design Guidelines everywhere, Rust SDK wrappers too (
IDnotId,URLnotUrl,configurationnotconfigorcfg). - File headers live in
IDETemplateMacros.plist.
- Code speak for itself — doc comments fine, but no comments restating what clear code already says.
- Do comment when code hides trap, hard to follow, or choice needs justifying.
- Comments short — one line where possible. Exceed only when truly needed.
- Default:
MXLog.info. Unexpected failures:.error. Noisy dev logs:.verbose..failure/.debugrare. - Never log secrets, passwords, keys, user content (e.g. message bodies).
- Action enums with secret associated values must conform
CustomStringConvertible(log only case name). - Matrix IDs safe to log.
- Default localisation:
en(en-GB strings), shared with Element X Android via Localazy. - Never edit
Localizable.strings— auto-overwritten. - New English strings →
Untranslated.strings(plurals:Untranslated.stringsdict). Team imports to Localazy before merge. - Access strings via generated
L10ntypes (e.g.L10n.actionDone). - Key naming (see element-x-android README):
- Cross-screen verbs:
action_. Nouns/other:common_. Accessibility:a11y_. - Key match string, e.g.
action_copy_link→Copy link. - Screen-specific:
screen_<name>_<free>(e.g.screen_onboarding_welcome_title). - Errors:
error_prefix or_error_infix. - iOS-only:
_iossuffix. Android-only:_androidsuffix. - Placeholders: always numbered form
%1$@,%1$d. Use%x$@in iOS source; add translator commentLocalazy: change %x$@ -> %x$s.
- Cross-screen verbs:
- Previews for all main states.
- Use
PreviewProvider(not#Preview) — snapshot/accessibility tests generated from it. - Add
TestablePreviewconformance → snapshot + accessibility tests generated.
- Sentence-style titles (no conventional commits).
- Exactly one
pr-label (see.github/release.yml). - Title = changelog entry — descriptive, no "Fixes #…".
- Leave description template for developer. Point them to contributing etiquette.
- Screenshots/videos for visual changes.
- 500 additions max — split big changes.
- Commits need title + description. No tiny commits, no massive commits.
- No history rewrites.
Initial setup: swift run tools setup-project
Git hooks installed by swift run tools setup-project, run SwiftLint/SwiftFormat on commit. Hook fail? Do not abandon changes — fix reported issues, recommit.
| Tool | Command | Notes |
|---|---|---|
| XcodeGen | xcodegen |
Generates Xcode project from project.yml (includes app.yml + target.yml files) |
| Sourcery | sourcery --config Tools/Sourcery/<config> |
Configs: AutoMockableConfig.yml, PreviewTestsConfig.yml, TestablePreviewsDictionary.yml, AccessibilityTests.yml. Auto-runs on ElementX build. |
| SwiftGen | swiftgen config run --config Tools/SwiftGen/swiftgen-config.yml |
Auto-runs on ElementX build. |
| SwiftLint | swiftlint |
Auto-runs on ElementX build |
| SwiftFormat | swiftformat . |
Run from project root only. Auto-runs in lint mode on ElementX build. |
CI test commands:
- Unit tests:
swift run tools ci unit-tests - CI help:
swift run tools ci --help
Key targets (each has target.yml):
- ElementX — main app
- NSE — Notification Service Extension
- ShareExtension — Share Extension
Each target: Sources/, Resources/, SupportingFiles/.
ElementX/Sources/
├── Application/ # App lifecycle, settings, windowing, root coordinators
├── FlowCoordinators/ # Flow coordinators + state machines
├── Services/<Feature>/ # SDK proxies, app services, non-view logic
├── Screens/<Screen>/ # View, ViewModel, Coordinator, Models per screen
├── Other/ # Shared extensions, utilities, SwiftUI helpers
└── {Unit|UI|A11y}Tests/ # Testing infrastructure
Mocks, test helpers, generated files live next to sources in Generated/ directories. Test suites in dedicated targets.
compound-ios/— Compound design system (local package, all UI styling).Tools/Sources/— Developer CLI helpers.
- matrix-rust-sdk source:
matrix-org/matrix-rust-sdk. - Binary builds (xcframework + Swift bindings):
element-hq/matrix-rust-components-swift, imported viaproject.yml. - SDK hash for build: cross-reference components version in
project.ymlwith its tag in components repo. Hash in commit message.
Every screen = MVVM-Coordinator. Template: Tools/Scripts/Templates/SimpleScreenExample/ + createScreen.sh.
| File | Purpose |
|---|---|
FooScreenModels.swift |
ViewState, ViewStateBindings, ViewAction enum, ViewModelAction enum |
FooScreenViewModelProtocol.swift |
Protocol: actionsPublisher, context |
FooScreenViewModel.swift |
Concrete VM subclassing StateStoreViewModelV2 |
FooScreen.swift (in View/) |
SwiftUI view taking @Bindable var context |
FooScreenCoordinator.swift |
Owns VM, subscribes to actions, exposes own actionsPublisher |
FooScreenViewModelTests.swift |
Unit tests (UnitTests target) |
View ──send(viewAction:)──► ViewModel ──actionsPublisher──► Coordinator ──actionsPublisher──► FlowCoordinator
◄──viewState────────── │
◄──$context.bindings──► StateMachine<State,Event>
Lives at ElementX/Sources/Other/SwiftUI/ViewModel/StateStoreViewModelV2.swift (uses Swift Observation):
state— mutable state struct conformingBindableStatecontext—@Observableclass passed to view:context.viewState— read-only statecontext.send(viewAction:)— sends action to view model$context.<binding>— two-way SwiftUI bindingscontext.mediaProvider— optional media service
- Override
process(viewAction:)for incoming actions. - Use
PassthroughSubject<ViewModelAction, Never>to notify coordinator.
Some screens still use older
StateStoreViewModel.swift(Combine/ObservableObject).
final class FooScreenCoordinator: CoordinatorProtocol {
private let parameters: FooScreenCoordinatorParameters
private let viewModel: FooScreenViewModelProtocol
private var cancellables = Set<AnyCancellable>()
private let actionsSubject: PassthroughSubject<FooScreenCoordinatorAction, Never> = .init()
var actionsPublisher: AnyPublisher<FooScreenCoordinatorAction, Never> { actionsSubject.eraseToAnyPublisher() }
init(parameters: FooScreenCoordinatorParameters) {
self.parameters = parameters
viewModel = FooScreenViewModel(/* dependencies */)
}
func start() {
viewModel.actionsPublisher.sink { [weak self] action in /* map to coordinator actions */ }
.store(in: &cancellables)
}
func toPresentable() -> AnyView { AnyView(FooScreen(context: viewModel.context)) }
}- SwiftUI Alerts — add
AlertInfotobindings. Present with one-liner in view. - User Indicators (via
UserIndicatorController):.toast— pill at top of screen (errors).modal— not for errors (or actual modal). Blocking overlay for waiting.
@MainActor protocol FlowCoordinatorProtocol {
func start(animated: Bool)
func handleAppRoute(_ appRoute: AppRoute, animated: Bool)
func clearRoute(animated: Bool)
}Uses StateMachine<State, Event> from ReactKit/SwiftState:
State/Eventenums defined inside flow coordinator, conformingStateType/EventType.- Simple transitions:
addRoutes(event:transitions:). Associated values:addRouteMapping. addErrorHandlershouldfatalErroron unexpected transitions.
| Type | SwiftUI Equivalent | Usage |
|---|---|---|
NavigationStackCoordinator |
NavigationStack |
Push/pop within flow |
NavigationSplitCoordinator |
NavigationSplitView |
iPad split view |
NavigationTabCoordinator |
TabView |
Tab navigation |
NavigationRootCoordinator |
Root view | Switch app root (e.g. auth → session) |
Shared dependency bag for flow coordinators only. Never pass to screen coordinators or view models — they get specific dependencies via their Parameters struct.
AppRoute enum = deep-link destinations. Handled in handleAppRoute by rebuilding coordinators, clearing stack, or no-op.
| Layer | Example | Location |
|---|---|---|
| Protocol | ClientProxyProtocol |
defines interface |
| Proxy | ClientProxy |
wraps SDK type, in Services/<Feature>/ |
| Mock | ClientProxyMock |
Sourcery-generated |
Naming: SDK type name + Proxy suffix (e.g. Client → ClientProxy). Exceptions where specialisation needed (e.g. JoinedRoomProxy, InvitedRoomProxy).
Map SDK types to app-owned Swift types (no MatrixRustSDK import in views):
init(rustValue:)— from SDKvar rustValue— back to SDK
Result<T, E>with typed errors (no barethrows).asynconly when Rust method async.- Prefer computed
varover methods for simple properties. - Map FFI types:
String→URL, timestampUInt64→Date, etc. - Follow Swift API naming (
userIDnotuserId).
Live in ElementX/Sources/Services/<Feature>/.
- Pure app services (e.g.
AppLockService) — no SDK involvement. - SDK-wrapping services — compose proxies with app logic. Keep view models simple/testable.
Services = where product-level opinions live (Rust SDK stays spec-faithful).
- Inject via
initparameters. - Screen coordinators get
Parametersstruct with specific dependencies. CommonFlowParametersflow-coordinator-only.ServiceLocatordeprecated — never use services directly. Always inject from above.
- All targets Swift 6.2, approachable concurrency.
- ElementX app, UnitTests + PreviewTests:
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor— no redundant@MainActor. Other targets (incl. app extensions) nonisolated. - Services + data-layer types:
nonisolated+Sendablewhen background work need. - Never
@unchecked Sendable/nonisolated(unsafe). Dev add these. actortypes rare in codebase.
compound-ios/ provides all UI styling. Use Compound. Deviate only when no equivalent exists.
- Colours:
Color.compound.textPrimary,.bgCanvasDefault, … - Fonts:
Font.compound.bodyLG,.headingMDBold,.bodySMSemibold, … - Icons: key paths on
CompoundIcons(e.g.\.userProfile). Always useCompoundIcon(\.iconName)(handles Dynamic Type scaling).
| Component | Notes |
|---|---|
ListRow |
Primary list/form building block. Label styles: .default, .plain, .action, .centeredAction. Kinds: .label, .button, .textField, .toggle, etc. |
.compoundList() |
Styles Form/List with Compound tokens |
.compoundListSectionHeader/Footer() |
Section header/footer styling |
CompoundButtonStyle |
Styles: .primary, .secondary, .tertiary, .super, .textLink. Sizes: .large, .medium, .small, .toolbarIcon |
CompoundToggleStyle |
.toggleStyle(.compound) |
CompoundIcon |
Sizes: .xSmall (16pt), .small (20pt), .medium (24pt), .custom(CGFloat) |
SendButton |
Specialised send button for message composition |
Label with icon keypaths |
Label("Title", icon: \.userProfile) uses CompoundIcon internally |
Form {
Section {
ListRow(label: .default(title: "Setting", icon: \.settings),
kind: .navigationLink { /* action */ })
ListRow(label: .default(title: "Toggle", icon: \.notifications),
details: .isWaiting(context.viewState.isLoading),
kind: .toggle($context.isEnabled))
}
}
.compoundList()
.navigationTitle("Settings")See compound-ios/Sources/Compound/ for full component set.
Coverage target: 80% (includes SDK, Compound, Rich Text Editor). Project migrating from XCTest to Swift Testing (see PR #5119).
| Type | Location | Purpose |
|---|---|---|
| Unit Tests | UnitTests/Sources/ |
VM logic, state machines, services |
| UI Tests | UITests/Sources/ |
Flow coordinator integration (snapshots) |
| Preview Tests | PreviewTests/Sources/ |
Auto-generated snapshots from previews |
| Accessibility Tests | AccessibilityTests/Sources/ |
Auto-generated Xcode Accessibility Audits |
No .serialized suite traits needed — tests not run in parallel.
Generated by Sourcery (configs in Tools/Sourcery/). Files in Generated/ directories.
- Un-configured methods crash on purpose.
- Common mocks have
Configuration-based convenienceinit:let mock = ClientProxyMock(.init(userID: "@alice:example.com"))
let deferred = deferFulfillment(context.observe(\.viewState.counter)) { $0 == 1 }
context.send(viewAction: .incrementCounter)
try await deferred.fulfill()
#expect(context.viewState.counter == 1)Same pattern for publishers.
- Stored in
<Target>/Sources/__Snapshots__/, tracked via Git LFS (git lfs install). - Re-record on correct device/OS if UI changes.
- Powered by
pointfreeco/swift-snapshot-testing. Failures produce 3 images: reference, failure, diff.
| File | Purpose |
|---|---|
project.yml |
XcodeGen project (targets, packages, settings) |
app.yml |
App-level XcodeGen config |
.swiftlint.yml |
SwiftLint rules |
.swiftformat |
SwiftFormat rules |
Dangerfile.swift |
Danger PR checks |
Package.swift |
SPM manifest (Tools CLI) |
localazy.json |
Localazy translation config |
codecov.yml |
Codecov config |
.periphery.yml |
Periphery dead-code detection |