Files
simplex-chat/apps/ios/spec/architecture.md
T
2026-03-05 09:13:24 +00:00

348 lines
20 KiB
Markdown

# SimpleX Chat iOS -- System Architecture
> Technical specification for the iOS app's layered architecture, FFI bridge, event system, and extension model.
>
> Related specs: [README](README.md) | [API Reference](api.md) | [State Management](state.md) | [Database](database.md)
> Related product: [Product Overview](../product/README.md)
**Source:** [`SimpleXApp.swift`](../Shared/SimpleXApp.swift#L1-L183) | [`AppDelegate.swift`](../Shared/AppDelegate.swift#L1-L209) | [`ContentView.swift`](../Shared/ContentView.swift#L1-L513) | [`ChatModel.swift`](../Shared/Model/ChatModel.swift#L1-L1373) | [`SimpleXAPI.swift`](../Shared/Model/SimpleXAPI.swift#L1-L2915) | [`AppAPITypes.swift`](../Shared/Model/AppAPITypes.swift#L1-L2357) | [`APITypes.swift`](../SimpleXChat/APITypes.swift#L1-L1071) | [`API.swift`](../SimpleXChat/API.swift#L1-L388)
---
## Table of Contents
1. [Layered Architecture](#1-layered-architecture)
2. [FFI Bridge](#2-ffi-bridge)
3. [Event Streaming](#3-event-streaming)
4. [Database Architecture](#4-database-architecture)
5. [App Lifecycle](#5-app-lifecycle)
6. [Extension Architecture](#6-extension-architecture)
7. [Remote Desktop Control](#7-remote-desktop-control)
---
## [1. Layered Architecture](../Shared/SimpleXApp.swift#L17-L184)
The app follows a strict layered model where each layer communicates only with its immediate neighbor:
```
┌─────────────────────────────────────────┐
│ SwiftUI Views │ Rendering, user interaction
│ (ChatListView, ChatView, ComposeView) │
├─────────────────────────────────────────┤
│ ChatModel (ObservableObject) │ App state, @Published properties
│ ItemsModel, Chat, ChatTagsModel │ Per-chat state, tag filtering
├─────────────────────────────────────────┤
│ SimpleXAPI (FFI Bridge) │ chatSendCmd/chatApiSendCmd
│ AppAPITypes (ChatCommand/Response) │ JSON serialization/deserialization
├─────────────────────────────────────────┤
│ C FFI Layer │ chat_send_cmd_retry, chat_recv_msg_wait
│ (SimpleX.h, libsimplex.a) │ Compiled Haskell via GHC cross-compiler
├─────────────────────────────────────────┤
│ Haskell Core (chat_ctrl) │ Chat logic, chat protocol (x-events),
│ (Simplex.Chat.Controller) │ database operations, file management
├─────────────────────────────────────────┤
│ simplexmq library (external) │ SMP/XFTP protocols, SMP Agent,
│ (github.com/simplex-chat/simplexmq) │ double-ratchet (PQDR), transport (TLS)
└─────────────────────────────────────────┘
```
**Key invariant**: No SwiftUI view directly calls FFI functions. All communication flows through `ChatModel` or dedicated API functions in `SimpleXAPI.swift`.
### Source Files
| Layer | File | Role | Line |
|-------|------|------|------|
| Views | [`Shared/Views/ChatList/ChatListView.swift`](../Shared/Views/ChatList/ChatListView.swift) | Chat list rendering | |
| Views | [`Shared/Views/Chat/ChatView.swift`](../Shared/Views/Chat/ChatView.swift) | Conversation rendering | |
| State | [`Shared/Model/ChatModel.swift`](../Shared/Model/ChatModel.swift#L337) | `ChatModel`, `ItemsModel`, `Chat` classes | L337, L74, L1271 |
| API | [`Shared/Model/SimpleXAPI.swift`](../Shared/Model/SimpleXAPI.swift#L93) | FFI bridge functions | L93 |
| API | [`Shared/Model/AppAPITypes.swift`](../Shared/Model/AppAPITypes.swift#L15) | `ChatCommand`, `ChatResponse`, `ChatEvent` enums | L15, L649, L1055 |
| FFI | [`SimpleXChat/SimpleX.h`](../SimpleXChat/SimpleX.h#L1-L49) | C header declaring Haskell exports | |
| FFI | [`SimpleXChat/APITypes.swift`](../SimpleXChat/APITypes.swift#L27) | `APIResult<R>`, `ChatError`, `ChatCmdProtocol` | L27, L699, L17 |
| Core | `../../src/Simplex/Chat/Controller.hs` | Haskell command processor — see `processCommand` in `Controller.hs` | |
---
## [2. FFI Bridge](../SimpleXChat/SimpleX.h#L1-L49)
### [C Functions (SimpleX.h)](../SimpleXChat/SimpleX.h#L1-L49)
The Haskell core exposes these C functions, declared in `SimpleXChat/SimpleX.h`:
```c
typedef void* chat_ctrl;
// Initialize database, apply migrations, return controller
char *chat_migrate_init_key(char *path, char *key, int keepKey, char *confirm,
int backgroundMode, chat_ctrl *ctrl);
// Send command string, return JSON response string
char *chat_send_cmd_retry(chat_ctrl ctl, char *cmd, int retryNum);
// Block until next async event arrives (or timeout)
char *chat_recv_msg_wait(chat_ctrl ctl, int wait);
// Close/reopen database store
char *chat_close_store(chat_ctrl ctl);
char *chat_reopen_store(chat_ctrl ctl);
// Utility: markdown parsing, server validation, password hashing
char *chat_parse_markdown(char *str);
char *chat_parse_server(char *str);
char *chat_password_hash(char *pwd, char *salt);
// File encryption/decryption
char *chat_write_file(chat_ctrl ctl, char *path, char *data, int len);
char *chat_read_file(char *path, char *key, char *nonce);
char *chat_encrypt_file(chat_ctrl ctl, char *fromPath, char *toPath);
char *chat_decrypt_file(char *fromPath, char *key, char *nonce, char *toPath);
```
### [Swift Bridge Functions (SimpleXAPI.swift)](../Shared/Model/SimpleXAPI.swift#L93-L221)
```swift
// Synchronous send -- blocks calling thread
func chatSendCmdSync<R: ChatAPIResult>(_ cmd: ChatCommand, bgTask: Bool = true,
bgDelay: Double? = nil, ctrl: chat_ctrl? = nil) throws -> R // L91
// Async send -- dispatches to background
func chatApiSendCmd<R: ChatAPIResult>(_ cmd: ChatCommand, bgTask: Bool = true,
bgDelay: Double? = nil, ctrl: chat_ctrl? = nil) async -> APIResult<R> // L215
// Low-level FFI call -- serializes command to string, calls chat_send_cmd_retry, decodes JSON
func sendSimpleXCmd<R: ChatAPIResult>(_ cmd: ChatCmdProtocol, _ ctrl: chat_ctrl?,
retryNum: Int32 = 0) -> APIResult<R> // SimpleXChat/API.swift L114
```
### Data Flow
1. Swift constructs a `ChatCommand` enum value (e.g., `.apiSendMessages(type:id:scope:live:ttl:composedMessages:)`)
2. [`ChatCommand.cmdString`](../Shared/Model/AppAPITypes.swift#L15) serializes it to a command string (e.g., `"/_send @1 json {...}"`)
3. [`sendSimpleXCmd`](../SimpleXChat/API.swift#L115) passes the string to `chat_send_cmd_retry` via C FFI
4. Haskell core processes the command, returns JSON response string
5. Swift decodes JSON into [`APIResult<R>`](../SimpleXChat/APITypes.swift#L27) where `R: ChatAPIResult`
6. Result is either `.result(R)`, `.error(ChatError)`, or `.invalid(type, json)`
### [Background Task Protection](../Shared/Model/SimpleXAPI.swift#L54-L79)
All FFI calls are wrapped in [`beginBGTask()`](../Shared/Model/SimpleXAPI.swift#L54) / `endBackgroundTask()` to prevent iOS from killing the app mid-operation. The `maxTaskDuration` is 15 seconds.
---
## [3. Event Streaming](../Shared/Model/SimpleXAPI.swift#L2220-L2916)
The Haskell core emits async events (new messages, connection status changes, file progress, etc.) that are not direct responses to commands. These are received via polling:
```
Haskell Core --[chat_recv_msg_wait]--> Swift event loop --> ChatModel update --> SwiftUI re-render
```
The event loop is implemented in [`ChatReceiver`](../Shared/Model/SimpleXAPI.swift#L2220-L2263), and events are dispatched by [`processReceivedMsg`](../Shared/Model/SimpleXAPI.swift#L2266).
### [Event Types (ChatEvent enum)](../Shared/Model/AppAPITypes.swift#L1055-L1129)
Key async events delivered from core to UI:
| Event | Description | Line |
|-------|-------------|------|
| `newChatItems` | New messages received | [L1070](../Shared/Model/AppAPITypes.swift#L1070) |
| `chatItemUpdated` | Message edited by sender | [L1072](../Shared/Model/AppAPITypes.swift#L1072) |
| `chatItemsDeleted` | Messages deleted | [L1074](../Shared/Model/AppAPITypes.swift#L1074) |
| `chatItemReaction` | Reaction added/removed | [L1073](../Shared/Model/AppAPITypes.swift#L1073) |
| `contactConnected` | New contact connected | [L1062](../Shared/Model/AppAPITypes.swift#L1062) |
| `contactUpdated` | Contact profile changed | [L1066](../Shared/Model/AppAPITypes.swift#L1066) |
| `receivedGroupInvitation` | Group invitation received | [L1077](../Shared/Model/AppAPITypes.swift#L1077) |
| `groupMemberUpdated` | Group member info changed | [L1067](../Shared/Model/AppAPITypes.swift#L1067) |
| `callInvitation` | Incoming call | [L1115](../Shared/Model/AppAPITypes.swift#L1115) |
| `chatSuspended` | Core suspended (background) | [L1056](../Shared/Model/AppAPITypes.swift#L1056) |
| `rcvFileComplete` | File download finished | [L1099](../Shared/Model/AppAPITypes.swift#L1099) |
| `sndFileCompleteXFTP` | File upload finished | [L1110](../Shared/Model/AppAPITypes.swift#L1110) |
Events are decoded as [`ChatEvent`](../Shared/Model/AppAPITypes.swift#L1055) enum in `Shared/Model/AppAPITypes.swift` and dispatched to update `ChatModel` / `ItemsModel` properties, triggering SwiftUI view re-renders via `@Published` property observation.
---
## [4. Database Architecture](../SimpleXChat/FileUtils.swift#L70-L294)
Two SQLite databases in the app group container (shared with NSE):
| Database | File | Contents |
|----------|------|----------|
| Chat DB | `simplex_v1_chat.db` | Messages, contacts, groups, profiles, files, tags, preferences |
| Agent DB | `simplex_v1_agent.db` | SMP connections, keys, queues, server info |
Both databases use the `DB_FILE_PREFIX = "simplex_v1"` prefix. The database path is resolved via [`getAppDatabasePath()`](../SimpleXChat/FileUtils.swift#L70) in `SimpleXChat/FileUtils.swift`, which checks `dbContainerGroupDefault` to determine whether to use the app group container or legacy documents directory.
See [Database & Storage specification](database.md) for full details.
---
## [5. App Lifecycle](../Shared/SimpleXApp.swift#L17-L184)
### [Initialization Sequence (SimpleXApp.swift)](../Shared/SimpleXApp.swift#L17-L38)
```swift
// SimpleXApp.init()
1. haskell_init() // Initialize Haskell RTS (background queue, sync)
2. UserDefaults.register(defaults:) // Register app preference defaults
3. setGroupDefaults() // Sync preferences to app group container
4. setDbContainer() // Set database path L122
5. BGManager.shared.register() // Register background task handlers
6. NtfManager.shared.registerCategories() // Register notification action categories
```
### State Transitions
```
┌──────────┐
│ Launched │
└─────┬─────┘
│ initChatAndMigrate()
v
┌──────────┐
│ DB Setup │ chat_migrate_init_key()
└─────┬─────┘
│ startChat() SimpleXAPI.swift L2098
v
┌──────────┐
│ Active │ apiActivateChat() SimpleXAPI.swift L358
└─────┬─────┘
│ scenePhase == .background
v
┌──────────┐
│Background │ apiSuspendChat(timeoutMicroseconds:) SimpleXAPI.swift L368
└─────┬─────┘
│ scenePhase == .active
v
┌──────────┐
│ Active │ startChatAndActivate()
└──────────┘
```
### [Scene Phase Handling (SimpleXApp.swift)](../Shared/SimpleXApp.swift#L38-L123)
- **`.active`**: Calls `startChatAndActivate()`, processes pending notification responses, refreshes chat list and call invitations
- **`.background`**: Records authentication timestamp, calls `suspendChat()` (unless CallKit call active), schedules `BGManager` background refresh, updates badge count
- **`.inactive`**: No explicit handling (transitional state)
### CallKit Exception
When a CallKit call is active during backgrounding, chat suspension is deferred (`CallController.shared.shouldSuspendChat = true`) until the call ends, to maintain the WebRTC session.
---
## [6. Extension Architecture](../SimpleX%20NSE/NotificationService.swift#L1-L1228)
### [Notification Service Extension (NSE)](../SimpleX%20NSE/NotificationService.swift#L1-L1228)
The NSE ([`SimpleX NSE/NotificationService.swift`](../SimpleX%20NSE/NotificationService.swift#L1-L1228)) is a separate process that:
1. Receives encrypted push notification payload from APNs
2. Initializes its own Haskell core instance (`chat_ctrl`) with shared database access
3. Decrypts the push payload using stored keys
4. Generates a visible `UNMutableNotificationContent` with the decrypted message preview
5. Delivers the notification to the user
**Database sharing**: Both main app and NSE access the same database files in the app group container (`APP_GROUP_NAME`). Coordination uses file locks to prevent concurrent write conflicts.
**Lifecycle**: The NSE has a ~30-second execution window per notification. It must initialize Haskell RTS, open the database, decrypt, and deliver within this window.
### Share Extension (SE)
The Share Extension (`SimpleX SE/`) allows sharing content (text, images, files) from other apps into SimpleX conversations.
---
## [7. Remote Desktop Control](../Shared/Views/RemoteAccess/ConnectDesktopView.swift#L1-L545)
Optional desktop pairing allows controlling the mobile app from a desktop client:
- **Pairing**: Encrypted QR code scanned by desktop client establishes a session
- **Commands**: [`connectRemoteCtrl`](../Shared/Model/SimpleXAPI.swift#L1613), [`findKnownRemoteCtrl`](../Shared/Model/SimpleXAPI.swift#L1620), [`confirmRemoteCtrl`](../Shared/Model/SimpleXAPI.swift#L1624), [`verifyRemoteCtrlSession`](../Shared/Model/SimpleXAPI.swift#L1630), [`listRemoteCtrls`](../Shared/Model/SimpleXAPI.swift#L1636), [`stopRemoteCtrl`](../Shared/Model/SimpleXAPI.swift#L1642), [`deleteRemoteCtrl`](../Shared/Model/SimpleXAPI.swift#L1646)
- **State**: [`ChatModel.remoteCtrlSession`](../Shared/Model/ChatModel.swift#L395)`: RemoteCtrlSession?` tracks the active session
- **Transport**: Encrypted reverse HTTP transport between mobile and desktop
- **Source**: [`Shared/Views/RemoteAccess/ConnectDesktopView.swift`](../Shared/Views/RemoteAccess/ConnectDesktopView.swift#L1-L545), see `Remote.hs` in `../../src/Simplex/Chat/`
---
## 8. Chat Relay Management
### Overview
Chat relays are SMP servers that forward messages to channel subscribers. They are configured in the Network & Servers settings and selected during channel creation.
### Data Model
| Type | Location | Description |
|------|----------|-------------|
| `UserChatRelay` | `ChatTypes.swift` | Relay server config: chatRelayId, address, name, domains, preset, tested, enabled, deleted |
| `UserOperatorServers.chatRelays` | `AppAPITypes.swift` | Array of `UserChatRelay` per operator |
| `UserServersWarning` | `AppAPITypes.swift` | Enum with `.noChatRelays(user:)` case |
| `ServerSettings.serverWarnings` | `ChatListView.swift` | `[UserServersWarning]` field on `ServerSettings` struct (exposed via `SaveableSettings.servers`) |
### Relay Management Views
| View | File | Description |
|------|------|-------------|
| `ChatRelayView` | `ChatRelayView.swift` | Edit/view relay: name, address, test, enable toggle, delete |
| `ChatRelayViewLink` | `ChatRelayView.swift` | NavigationLink row showing relay status icon + display name |
| `NewChatRelayView` | `ChatRelayView.swift` | Form to add new relay (name + address + test + enable toggle) |
| `ServersWarningView` | `NetworkAndServers.swift` | Orange exclamation triangle + warning text |
### Key Functions
| Function | File | Description |
|----------|------|-------------|
| `addChatRelay(...)` | `ChatRelayView.swift` | Validates name/address, appends to `userServers[nil operator].chatRelays`, calls `validateServers_` |
| `deleteChatRelay(...)` | `ProtocolServersView.swift` | Marks relay as deleted or removes if no `chatRelayId` |
| `validRelayName(_:)` | `ChatRelayView.swift` | Non-empty + valid display name check |
| `validRelayAddress(_:)` | `ChatRelayView.swift` | Parses via `parseSimpleXMarkdown`, validates `.simplexLink(_, .relay, _, _)` |
| `showRelayTestStatus(relay:)` | `ChatRelayView.swift` | ViewBuilder returning checkmark/multiply/clear icons |
| `validateServers_` | `NetworkAndServers.swift` | Extended signature: now accepts optional `Binding<[UserServersWarning]>?`; calls `validateServers` which returns `([UserServersError], [UserServersWarning])` tuple |
| `globalServersWarning(_:)` | `NetworkAndServers.swift` | Extracts `.noChatRelays` warning text for display |
| `bindingForChatRelays(_:_:)` | `NetworkAndServers.swift` | Creates binding for `chatRelays` at operator index |
### Relay Sections in Settings
"Chat relays" sections appear in:
- `OperatorView`: lists relays for the operator, with header and footer
- `YourServersView` (in `ProtocolServersView`): lists relays for non-operator servers, with delete support and "Add server" -> "Chat relay" option
### serverWarnings Plumbing
`Binding<[UserServersWarning]>` is threaded through: `NetworkAndServers` -> `OperatorView` -> `ProtocolServersView` -> `ProtocolServerView` / `NewServerView` / `ScanProtocolServer`. All `validateServers_` calls pass the warnings binding.
---
## Source Files
| File | Path | Line |
|------|------|------|
| App entry point | [`Shared/SimpleXApp.swift`](../Shared/SimpleXApp.swift#L17) | L17 |
| App delegate | [`Shared/AppDelegate.swift`](../Shared/AppDelegate.swift#L15) | L15 |
| Root view | [`Shared/ContentView.swift`](../Shared/ContentView.swift#L24) | L24 |
| FFI bridge | [`Shared/Model/SimpleXAPI.swift`](../Shared/Model/SimpleXAPI.swift#L93) | L93 |
| Low-level FFI | [`SimpleXChat/API.swift`](../SimpleXChat/API.swift#L115) | L115 |
| App state | [`Shared/Model/ChatModel.swift`](../Shared/Model/ChatModel.swift#L337) | L337 |
| API types | [`Shared/Model/AppAPITypes.swift`](../Shared/Model/AppAPITypes.swift#L15) | L15 |
| Shared types | [`SimpleXChat/APITypes.swift`](../SimpleXChat/APITypes.swift#L27) | L27 |
| C header | [`SimpleXChat/SimpleX.h`](../SimpleXChat/SimpleX.h#L1-L49) | |
| NSE | [`SimpleX NSE/NotificationService.swift`](../SimpleX%20NSE/NotificationService.swift#L1-L1228) | |
| Haskell core | `../../src/Simplex/Chat/Controller.hs` — see `processCommand` in `Controller.hs` | |
| Chat protocol (x-events, message envelopes) | `../../src/Simplex/Chat/Protocol.hs` | |
### External: simplexmq Library
The lower-level protocol and encryption layers are in the separate [simplexmq](https://github.com/simplex-chat/simplexmq) library:
| Component | Spec | Implementation |
|-----------|------|----------------|
| SMP protocol | `simplexmq/protocol/simplex-messaging.md` | `simplexmq/src/Simplex/Messaging/Protocol.hs` |
| XFTP protocol | `simplexmq/protocol/xftp.md` | `simplexmq/src/Simplex/FileTransfer/Protocol.hs` |
| SMP Agent (duplex connections) | `simplexmq/protocol/agent-protocol.md` | `simplexmq/src/Simplex/Messaging/Agent.hs` |
| Double ratchet (PQDR) | `simplexmq/protocol/pqdr.md` | `simplexmq/src/Simplex/Messaging/Crypto/Ratchet.hs` |
| Post-quantum KEM (sntrup761) | `simplexmq/protocol/pqdr.md` | `simplexmq/src/Simplex/Messaging/Crypto/SNTRUP761.hs` |
| TLS transport | — | `simplexmq/src/Simplex/Messaging/Transport.hs` |
| File encryption | — | `simplexmq/src/Simplex/Messaging/Crypto/File.hs` |