Four fixes from adversarial review of the branch. ios: clamp the unread counter at the apply point. changeUnreadCounter(chatIndex:) applied the delta unclamped, so unreadCount could go negative and the app icon badge with it - a state that persists until the chat list reloads and that also suppresses the badge for genuinely unread messages arriving after. Reachable because the edit/delete sync added removeChatItem as a decrement source for member support items, while the count being decremented excludes them (the chat list queries filter group_scope_tag IS NULL): after a restart, moderating a still-unread support message decrements a zero. Clamped at the collector flush - its only caller - so it is correct regardless of how the debounced deltas batch, and the user/badge counter is fed the delta actually applied. Android has always done this in decreaseCounterInPrimaryContext. android, desktop: gate both primary-context mirrors on memberPending. The primary's preview block runs for a scoped ChatInfo only when memberPending, so for anyone else the mirrored call could not update a preview - it only had side effects. addChatItem's mirror re-ordered the group in the main chat list when a non-pending member replied in a member support chat, and, lacking a scope check, also re-ran the primary for sent items the dispatcher had already given it. ComposeView's edit mirror is on the live message path, so it fired on every tick to do nothing. android, desktop, ios: don't let upsertChatItem create a chat entry from a scoped ChatInfo. The memberPending exception made its "chat not found" branch reachable for support items, seeding the main list with a support item as preview - and since ChatInfo.Group.id ignores the scope, shadowing the real group rather than showing as a duplicate. On iOS it also set itemAdded, which chatItemSimpleUpdate turns into a new message notification. addChatItem never did this either. android, desktop, ios: gate the media preview exception on showChatPreviews. Treating a caption-less media support message as "has content" skips the status branch, but the next branch needs showChatPreviews and Android/desktop have no trailing else (the thumbnail is gated by the same flag) - so with previews off the row rendered empty where master showed "reviewed by admins". iOS redacted it rather than dropping it, but also lost the status. Falls back to hasMsgContent when previews are off.
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