SimpleX Chat iOS -- State Management
Source: ChatModel.swift | ChatTypes.swift
Technical specification for the app's state architecture: ChatModel, ItemsModel, Chat, ChatInfo, and preference storage.
Related specs: Architecture | API Reference | README
Related product: Concept Index
Table of Contents
- Overview
- ChatModel -- Primary App State
- ItemsModel -- Per-Chat Message State
- ChatTagsModel -- Tag Filtering State
- Chat -- Single Conversation State
- ChatInfo -- Conversation Metadata
- State Flow
- Preference Storage
1. Overview
The app uses SwiftUI's ObservableObject pattern for reactive state management. The state hierarchy is:
Class: final class ChatModel: ObservableObject
Singleton: ChatModel.shared
Source: Shared/Model/ChatModel.swift
Key Published Properties
App Lifecycle
| Property |
Type |
Description |
Line |
onboardingStage |
OnboardingStage? |
Current onboarding step |
L331 |
chatInitialized |
Bool |
Whether chat has been initialized |
L340 |
chatRunning |
Bool? |
Whether chat engine is running |
L341 |
chatDbChanged |
Bool |
Whether DB was changed externally |
L342 |
chatDbEncrypted |
Bool? |
Whether DB is encrypted |
L343 |
chatDbStatus |
DBMigrationResult? |
DB migration status |
L344 |
ctrlInitInProgress |
Bool |
Whether controller is initializing |
L345 |
migrationState |
MigrationToState? |
Device migration state |
L390 |
User State
| Property |
Type |
Description |
Line |
currentUser |
User? |
Active user profile (triggers theme reapply on change) |
L334 |
users |
[UserInfo] |
All user profiles |
L339 |
v3DBMigration |
V3DBMigrationState |
Legacy DB migration state |
L333 |
Chat List
| Property |
Type |
Description |
Line |
chats |
[Chat] (private set) |
All conversations for current user |
L351 |
deletedChats |
Set<String> |
Chat IDs pending deletion animation |
L352 |
Active Chat
| Property |
Type |
Description |
Line |
chatId |
String? |
Currently open chat ID |
L354 |
chatAgentConnId |
String? |
Agent connection ID for active chat |
L355 |
chatSubStatus |
SubscriptionStatus? |
Active chat subscription status |
L356 |
openAroundItemId |
ChatItem.ID? |
Item to scroll to when opening |
L357 |
chatToTop |
String? |
Chat to scroll to top |
L358 |
groupMembers |
[GMember] |
Members of active group |
L359 |
groupMembersIndexes |
[Int64: Int] |
Member ID to index mapping |
L360 |
membersLoaded |
Bool |
Whether members have been loaded |
L361 |
secondaryIM |
ItemsModel? |
Secondary items model (e.g. support chat scope) |
L408 |
Authentication
| Property |
Type |
Description |
Line |
contentViewAccessAuthenticated |
Bool |
Whether user has passed authentication |
L348 |
laRequest |
LocalAuthRequest? |
Pending authentication request |
L349 |
Notifications
| Property |
Type |
Description |
Line |
deviceToken |
DeviceToken? |
Current APNs device token |
L369 |
savedToken |
DeviceToken? |
Previously saved token |
L370 |
tokenRegistered |
Bool |
Whether token is registered with server |
L371 |
tokenStatus |
NtfTknStatus? |
Token registration status |
L373 |
notificationMode |
NotificationsMode |
Current notification mode (.off/.periodic/.instant) |
L374 |
notificationServer |
String? |
Notification server URL |
L375 |
notificationPreview |
NotificationPreviewMode |
What to show in notifications |
L376 |
notificationResponse |
UNNotificationResponse? |
Pending notification action |
L346 |
ntfContactRequest |
NTFContactRequest? |
Pending contact request from notification |
L378 |
ntfCallInvitationAction |
(ChatId, NtfCallAction)? |
Pending call action from notification |
L379 |
Calls
| Property |
Type |
Description |
Line |
callInvitations |
[ChatId: RcvCallInvitation] |
Pending incoming call invitations |
L381 |
activeCall |
Call? |
Currently active call |
L382 |
callCommand |
WebRTCCommandProcessor |
WebRTC command queue |
L383 |
showCallView |
Bool |
Whether to show full-screen call UI |
L384 |
activeCallViewIsCollapsed |
Bool |
Whether call view is in PiP mode |
L385 |
Remote Desktop
| Property |
Type |
Description |
Line |
remoteCtrlSession |
RemoteCtrlSession? |
Active remote desktop session |
L387 |
Misc
| Property |
Type |
Description |
Line |
userAddress |
UserContactLink? |
User's SimpleX address |
L365 |
chatItemTTL |
ChatItemTTL |
Global message TTL |
L366 |
appOpenUrl |
URL? |
URL opened while app active |
L367 |
appOpenUrlLater |
URL? |
URL opened while app inactive |
L368 |
showingInvitation |
ShowingInvitation? |
Currently displayed invitation |
L389 |
draft |
ComposeState? |
Saved compose draft |
L393 |
draftChatId |
String? |
Chat ID for saved draft |
L394 |
networkInfo |
UserNetworkInfo |
Current network type and status |
L395 |
conditions |
ServerOperatorConditions |
Server usage conditions |
L397 |
stopPreviousRecPlay |
URL? |
Currently playing audio source |
L392 |
Non-Published Properties
| Property |
Type |
Description |
Line |
messageDelivery |
[Int64: () -> Void] |
Pending delivery confirmation callbacks |
L399 |
filesToDelete |
Set<URL> |
Files queued for deletion |
L401 |
im |
ItemsModel |
Reference to ItemsModel.shared |
L405 |
Key Methods
| Method |
Description |
Line |
getUser(_ userId:) |
Find user by ID |
L427 |
updateUser(_ user:) |
Update user in list and current |
L437 |
removeUser(_ user:) |
Remove user from list |
L446 |
getChat(_ id:) |
Find chat by ID |
L456 |
addChat(_ chat:) |
Add chat to list |
L510 |
updateChatInfo(_ cInfo:) |
Update chat metadata |
L523 |
replaceChat(_ id:, _ chat:) |
Replace chat in list |
L574 |
removeChat(_ id:) |
Remove chat from list |
L1180 |
popChat(_ id:, _ ts:) |
Move chat to top of list |
L1157 |
totalUnreadCountForAllUsers() |
Sum unread across all users |
L1058 |
Class: class ItemsModel: ObservableObject
Primary singleton: ItemsModel.shared
Secondary instances: Created via ItemsModel.loadSecondaryChat() for scope-based views (e.g., group member support chat)
Source: Shared/Model/ChatModel.swift
Properties
| Property |
Type |
Description |
Line |
reversedChatItems |
[ChatItem] |
Messages in reverse chronological order (newest first) |
L78 |
itemAdded |
Bool |
Flag indicating a new item was added |
L81 |
chatState |
ActiveChatState |
Pagination splits and loaded ranges |
L85 |
isLoading |
Bool |
Whether messages are currently loading |
L89 |
showLoadingProgress |
ChatId? |
Chat ID showing loading spinner |
L90 |
preloadState |
PreloadState |
State for infinite-scroll preloading |
L75 |
secondaryIMFilter |
SecondaryItemsModelFilter? |
Filter for secondary instances |
L74 |
Computed Properties
| Property |
Type |
Description |
Line |
lastItemsLoaded |
Bool |
Whether the oldest messages have been loaded |
L95 |
contentTag |
MsgContentTag? |
Content type filter (if secondary) |
L154 |
groupScopeInfo |
GroupChatScopeInfo? |
Group scope filter (if secondary) |
L162 |
Throttling
ItemsModel uses a custom publisher throttle (0.2 seconds) to batch rapid updates to reversedChatItems and prevent excessive SwiftUI re-renders:
Direct @Published properties (isLoading, showLoadingProgress) bypass throttling for immediate UI response.
Key Methods
| Method |
Description |
Line |
loadOpenChat(_ chatId:) |
Load chat with 250ms navigation delay |
L113 |
loadOpenChatNoWait(_ chatId:, _ openAroundItemId:) |
Load chat without delay |
L138 |
loadSecondaryChat(_ chatId:, chatFilter:) |
Create secondary ItemsModel instance |
L107 |
Used for secondary chat views (e.g., group member support scope, content type filter):
Class: class ChatTagsModel: ObservableObject
Singleton: ChatTagsModel.shared
Source: Shared/Model/ChatModel.swift
Properties
| Property |
Type |
Description |
Line |
userTags |
[ChatTag] |
User-defined tags |
L186 |
activeFilter |
ActiveFilter? |
Currently active filter tab |
L187 |
presetTags |
[PresetTag: Int] |
Preset tag counts (groups, contacts, favorites, etc.) |
L188 |
unreadTags |
[Int64: Int] |
Unread count per user tag |
L189 |
Class: final class Chat: ObservableObject, Identifiable, ChatLike
Source: Shared/Model/ChatModel.swift
Represents a single conversation in the chat list. Each Chat is an independent observable object.
Properties
| Property |
Type |
Description |
Line |
chatInfo |
ChatInfo |
Conversation type and metadata |
L1253 |
chatItems |
[ChatItem] |
Preview items (typically last message) |
L1254 |
chatStats |
ChatStats |
Unread counts and min unread item ID |
L1255 |
created |
Date |
Creation timestamp |
L1256 |
Computed Properties
| Property |
Description |
Line |
id |
Chat ID from chatInfo.id |
L1287 |
viewId |
Unique view identity including creation time |
L1289 |
unreadTag |
Whether chat counts as "unread" based on notification settings |
L1279 |
supportUnreadCount |
Unread count for group support scope |
L1291 |
Enum: public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable
Source: SimpleXChat/ChatTypes.swift
Represents the type and metadata of a conversation:
Cases
| Case |
Associated Value |
Description |
.direct |
Contact |
One-to-one conversation |
.group |
GroupInfo, GroupChatScopeInfo? |
Group conversation (optional scope for member support threads) |
.local |
NoteFolder |
Local notes (self-chat) |
.contactRequest |
UserContactRequest |
Incoming contact request |
.contactConnection |
PendingContactConnection |
Pending connection |
.invalidJSON |
Data? |
Undecodable chat data |
Key Computed Properties on ChatInfo
| Property |
Type |
Description |
chatType |
ChatType |
.direct, .group, .local, .contactRequest, .contactConnection |
id |
ChatId |
Prefixed ID (e.g., "@1" for direct, "#5" for group) |
displayName |
String |
Contact/group name |
image |
String? |
Profile image (base64) |
chatSettings |
ChatSettings? |
Notification/favorite settings |
chatTags |
[Int64]? |
Assigned tag IDs |
7. State Flow
App Start
Opening a Chat
Receiving a Message (Event)
Sending a Message
8. Preference Storage
UserDefaults (via @AppStorage)
App-level UI settings stored in UserDefaults.standard:
| Key Constant |
Type |
Description |
DEFAULT_PERFORM_LA |
Bool |
Enable local authentication |
DEFAULT_PRIVACY_PROTECT_SCREEN |
Bool |
Hide screen in app switcher |
DEFAULT_SHOW_LA_NOTICE |
Bool |
Show LA setup notice |
DEFAULT_NOTIFICATION_ALERT_SHOWN |
Bool |
Notification permission alert shown |
DEFAULT_CALL_KIT_CALLS_IN_RECENTS |
Bool |
Show CallKit calls in recents |
GroupDefaults
Settings shared between main app and extensions (NSE, SE) via app group UserDefaults:
| Key |
Description |
appStateGroupDefault |
Current app state (.active/.suspended/.stopped) |
dbContainerGroupDefault |
Database container location (.group/.documents) |
ntfPreviewModeGroupDefault |
Notification preview mode |
storeDBPassphraseGroupDefault |
Whether to store DB passphrase |
callKitEnabledGroupDefault |
Whether CallKit is enabled |
onboardingStageDefault |
Current onboarding stage |
currentThemeDefault |
Current theme name |
systemDarkThemeDefault |
Dark mode theme name |
themeOverridesDefault |
Custom theme overrides |
currentThemeIdsDefault |
Active theme override IDs |
Keychain (KeyChain wrapper)
Sensitive data stored in iOS Keychain:
| Key |
Description |
kcDatabasePassword |
SQLite database encryption key |
kcAppPassword |
App lock password |
kcSelfDestructPassword |
Self-destruct trigger password |
Haskell DB (via apiSaveSettings / apiGetSettings)
Chat-level preferences stored in the SQLite database (managed by Haskell core):
- Per-contact preferences (timed messages, voice, calls, etc.)
- Per-group preferences
- Per-user notification settings
- Network configuration
- Server lists
Source Files