The count is seeded by getUsersInfo, which counts RcvNew items where ntfsEnabled holds - every item for an unmuted chat, only mentions for a mentions-only one, none for a muted one. The deltas that maintain it afterwards did not follow that rule for the active profile: addChatItem incremented for every item regardless of the chat's notification setting, the mark-read helpers subtracted the full unread count, removeChat left a deleted chat's unread behind, and changing the setting moved a chat between rules without moving the count. Chat.userUnreadCount names what a chat contributes, and it is not a new rule: totalUnreadCountForAllUsers already computed exactly this inline for the app badge, and now calls it. Every site that changes a chat's stats, its settings or its presence reports the contribution before and after, and the count moves by the difference. For that to hold the stats it reads have to be maintained, so on Android and desktop decreaseCounterInPrimaryContext now decrements unreadMentions alongside unreadCount when the item was a mention. Without it a mentions-only chat's contribution never moved on any of that function's three paths - a deleted item, a moderated member item, or an item going from New to Read. iOS already passed the mention delta on its deletion paths. This is visible beyond the profile picker, which badges only other profiles: the desktop tray dot sums the count across profiles including the active one, and on iOS these same deltas drive the app icon badge. So on iOS, muting a chat with unread now drops the badge by its contribution, deleting a chat drops it, and a message in a muted chat no longer raises it - each of which now agrees with totalUnreadCountForAllUsers, which the badge is also set from. addChat is deliberately not the mirror of removeChat: adding a chat means it is being loaded into view, and its items were already counted by the seed, so counting them again on ChatItemsLoader's addChat would double them. The one place that removes and re-adds a chat while keeping its items - deleting a contact in "keep conversation" mode on iOS - now carries its stats across and restores the contribution explicitly, since the core keeps both the contact row and its items there and the seed still counts them. Wherever the chat list is re-read wholesale the counts are re-read with it: both resume paths, and setting a chat item TTL, which deletes items across every profile and so leaves every profile's count stale, not just the active one. They are assigned before the chats, which matters on iOS because updateChats recomputes the app badge from users there. Reading users is best effort on both platforms - losing them must not cost us the chats already fetched. This does not make the count exact. getUsersInfo has no group_scope_tag filter where the query behind chatStats.unreadCount does, so the seed counts support-scope items that no client-side rule can, and the count steps up by the outstanding support unread on each reseed. Two paths also install a whole chatStats without moving the count: processLoadedChat's updateChatStats on Android and desktop, and replaceChat on both - from a pre-call snapshot in the mark-read paths there, and from the server in iOS's contact-request handler.
SimpleX Chat iOS app
This file provides guidance when working with code in this repository.
iOS App Overview
The iOS app is a SwiftUI application that interfaces with the Haskell core library via FFI. It shares the SimpleXChat framework with two extensions: Notification Service Extension (NSE) for push notifications and Share Extension (SE) for sharing content from other apps.
Build & Development
Open SimpleX.xcodeproj in Xcode. The project has five targets:
- SimpleX (iOS) - Main app (Bundle ID:
chat.simplex.app) - SimpleXChat - Framework containing FFI bridge and shared types
- SimpleX NSE - Notification Service Extension
- SimpleX SE - Share Extension
- Tests iOS - UI tests
Build and run via Xcode (Product > Build/Run). Tests run via Product > Test or:
xcodebuild test -scheme "SimpleX (iOS)" -destination 'platform=iOS Simulator,name=iPhone 15'
Deployment target: iOS 15.0+, Swift 5.0.
Architecture
Haskell Core Integration
The app calls the Haskell core library through C FFI defined in SimpleXChat/SimpleX.h:
chat_migrate_init_key()- Initialize/migrate databasechat_send_cmd_retry()- Send command to chat controllerchat_recv_msg_wait()- Receive messages from controller
Swift wrappers in SimpleXChat/API.swift:
chatMigrateInit()- Initialize chat controllersendSimpleXCmd<R>()- Send typed commands and parse responsesrecvSimpleXMsg<R>()- Receive typed messages
Haskell runtime initialization (SimpleXChat/hs_init.c) uses different memory configurations:
- Main app: 64MB heap
- NSE: 512KB heap (minimal footprint for background processing)
- SE: 1MB heap
Pre-compiled Haskell libraries are in Libraries/{ios,mac,sim}/.
State Management
- ChatModel (
Shared/Model/ChatModel.swift) - Main singletonObservableObjectfor app-wide state (chat list, active chat, users) - ItemsModel - Manages chat items within a selected chat (similar to Kotlin's ChatsContext)
- AppTheme - Theme management and customization
App Structure
Entry point: Shared/SimpleXApp.swift
Key directories in Shared/:
Model/- Data models and API layer (ChatModel.swift,SimpleXAPI.swift)Views/- SwiftUI views organized by feature:ChatList/- Chat list and user pickerChat/- Message display and compositionCall/- VoIP call UIUserSettings/- App settingsLocalAuth/- Passcode and biometric authenticationDatabase/- Database initialization and migration
Shared Data Between Targets
All three targets share data via App Group (group.chat.simplex.app):
SimpleXChat/AppGroup.swift- GroupDefaults wrapper for typed shared preferences- Keychain for sensitive data:
kcDatabasePassword,kcAppPassword,kcSelfDestructPassword
Key Types
Types are defined in SimpleXChat/:
ChatTypes.swift- User, Chat, Message, Group typesAPITypes.swift- API request/response types
Commands follow ChatCmdProtocol (has cmdString property), sent as JSON through FFI.
Localization
31 languages supported. Localization files in SimpleX Localizations/.
Workflow:
Product > Export Localizations- Export XLIFF filesProduct > Import Localizations- Import updated translations
SimpleX Assets
The app includes optional assets behind the SIMPLEX_ASSETS Swift compilation flag. Without setup, the app builds normally without them.
Setup
Create Local.xcconfig (gitignored) in the apps/ios/ directory:
SIMPLEX_ASSETS_DIR = /path/to/assets
SWIFT_ACTIVE_COMPILATION_CONDITIONS = $(inherited) SIMPLEX_ASSETS
The copy script (scripts/ios/copy-assets.sh) runs as a build phase on each build but exits immediately if SIMPLEX_ASSETS is not set.
Updating assets
When source images change, regenerate resized images (requires ImageMagick):
cd path/to/assets && ./resize.sh
Background Capabilities
Configured in Info.plist:
- Background modes: audio, fetch, remote-notification, voip
- URL scheme:
simplex://for deep linking - BGTaskScheduler:
chat.simplex.app.receive