mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-08-15 17:50:22 +00:00
381 lines
16 KiB
Markdown
381 lines
16 KiB
Markdown
# SimpleX Chat iOS -- Navigation Architecture
|
|
|
|
> Technical specification for the navigation stack, deep linking, sheet presentation, and call overlay.
|
|
>
|
|
> Related specs: [Chat List](chat-list.md) | [Chat View](chat-view.md) | [State Management](../state.md) | [README](../README.md)
|
|
> Related product: [Product Overview](../../product/README.md)
|
|
|
|
**Source:** [`ContentView.swift`](../../Shared/ContentView.swift) | [`NewChatView.swift`](../../Shared/Views/NewChat/NewChatView.swift) | [`SettingsView.swift`](../../Shared/Views/UserSettings/SettingsView.swift) | [`OnboardingView.swift`](../../Shared/Views/Onboarding/OnboardingView.swift) | [`UserProfilesView.swift`](../../Shared/Views/UserSettings/UserProfilesView.swift)
|
|
|
|
---
|
|
|
|
## Table of Contents
|
|
|
|
1. [Overview](#1-overview)
|
|
2. [Root View -- ContentView](#2-root-view)
|
|
3. [Navigation Stack](#3-navigation-stack)
|
|
4. [Sheet Presentation](#4-sheet-presentation)
|
|
5. [Deep Linking](#5-deep-linking)
|
|
6. [Call Overlay](#6-call-overlay)
|
|
7. [Authentication Gate](#7-authentication-gate)
|
|
8. [Onboarding Flow](#8-onboarding-flow)
|
|
|
|
---
|
|
|
|
## 1. Overview
|
|
|
|
The app's navigation follows a hierarchical model with a single navigation stack rooted in `ContentView`. Modal sheets and full-screen overlays augment the primary navigation path.
|
|
|
|
```
|
|
SimpleXApp
|
|
└── ContentView (root)
|
|
├── Authentication gate (LocalAuthView / SetAppPasscodeView)
|
|
├── Onboarding flow (if first launch / migration)
|
|
├── Main content
|
|
│ └── NavigationStack / NavigationView
|
|
│ ├── ChatListView (root of stack)
|
|
│ │ ├── ChatView (pushed)
|
|
│ │ │ ├── ChatInfoView / GroupChatInfoView (pushed)
|
|
│ │ │ └── ChatItemInfoView (pushed)
|
|
│ │ └── ContactConnectionInfo (pushed)
|
|
│ └── Settings views (pushed)
|
|
├── Sheets (modal)
|
|
│ ├── UserPicker
|
|
│ ├── NewChatView
|
|
│ ├── WhatsNew / Notices
|
|
│ └── Settings sub-views
|
|
└── Overlays (always on top)
|
|
├── Active call banner (when call active)
|
|
└── ActiveCallView (full-screen call)
|
|
```
|
|
|
|
---
|
|
|
|
## 2. Root View -- [`ContentView`](../../Shared/ContentView.swift#L24)
|
|
|
|
**File**: [`Shared/ContentView.swift`](../../Shared/ContentView.swift)
|
|
|
|
`ContentView` is the root view injected by `SimpleXApp`. It manages:
|
|
|
|
### [Environment](../../Shared/ContentView.swift#L25-L37)
|
|
- `@EnvironmentObject var chatModel: ChatModel`
|
|
- `@EnvironmentObject var theme: AppTheme`
|
|
- `@Environment(\.scenePhase) var scenePhase`
|
|
|
|
### [Key State](../../Shared/ContentView.swift#L35-L52)
|
|
| Property | Type | Purpose |
|
|
|----------|------|---------|
|
|
| [`contentAccessAuthenticationExtended`](../../Shared/ContentView.swift#L35) | `Bool` | Passed at init to avoid re-render timing issues |
|
|
| [`automaticAuthenticationAttempted`](../../Shared/ContentView.swift#L38) | `Bool` | Whether biometric auth was auto-attempted |
|
|
| [`waitingForOrPassedAuth`](../../Shared/ContentView.swift#L51) | `Bool` | Whether auth gate should show |
|
|
| [`chatListUserPickerSheet`](../../Shared/ContentView.swift#L52) | `UserPickerSheet?` | Active user picker sheet |
|
|
|
|
### [View Selection Logic](../../Shared/ContentView.swift#L60-L80)
|
|
|
|
```swift
|
|
// Simplified decision tree in ContentView.body:
|
|
if !prefPerformLA || accessAuthenticated {
|
|
contentView() // Main app content
|
|
} else {
|
|
lockButton() // Authentication required
|
|
}
|
|
```
|
|
|
|
The [`contentView()`](../../Shared/ContentView.swift#L169) function further decides:
|
|
- If `chatModel.onboardingStage != .onboardingComplete`: show [onboarding](../../Shared/ContentView.swift#L174)
|
|
- If `chatModel.migrationState != nil`: show migration UI
|
|
- Otherwise: show `ChatListView` in a navigation container
|
|
|
|
---
|
|
|
|
## 3. Navigation Stack
|
|
|
|
### iOS Version Compatibility
|
|
|
|
**File**: [`Shared/Views/Helpers/NavStackCompat.swift`](../../Shared/Views/Helpers/NavStackCompat.swift)
|
|
|
|
The app supports iOS 15+ and uses a compatibility wrapper ([`NavStackCompat`](../../Shared/Views/Helpers/NavStackCompat.swift#L11)):
|
|
|
|
```swift
|
|
// NavStackCompat provides:
|
|
// - NavigationStack (iOS 16+): programmatic navigation via NavigationPath
|
|
// - NavigationView (iOS 15): classic NavigationLink-based navigation
|
|
```
|
|
|
|
### Primary Navigation Path
|
|
|
|
```
|
|
ChatListView
|
|
│
|
|
├─[tap chat]─→ ChatView
|
|
│ │
|
|
│ ├─[tap info]─→ ChatInfoView (direct)
|
|
│ │ └─→ VerifyCodeView, etc.
|
|
│ │
|
|
│ ├─[tap info]─→ GroupChatInfoView (group)
|
|
│ │ ├─→ GroupMemberInfoView
|
|
│ │ ├─→ GroupProfileView
|
|
│ │ └─→ GroupLinkView
|
|
│ │
|
|
│ └─[tap message info]─→ ChatItemInfoView
|
|
│
|
|
├─[tap connection]─→ ContactConnectionInfo
|
|
│
|
|
└─[settings]─→ SettingsView
|
|
├─→ NotificationsView
|
|
├─→ NetworkAndServers
|
|
├─→ AppearanceSettings
|
|
├─→ PrivacySettings
|
|
├─→ DatabaseView
|
|
└─→ UserProfilesView
|
|
```
|
|
|
|
### Navigation Trigger
|
|
|
|
Chat navigation is triggered by setting `ChatModel.chatId`:
|
|
|
|
```swift
|
|
// In ChatListNavLink:
|
|
ItemsModel.shared.loadOpenChat(chatId) {
|
|
// This sets ChatModel.chatId = chatId after a 250ms delay
|
|
// allowing navigation animation to start smoothly
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 4. Sheet Presentation
|
|
|
|
Sheets are presented modally on top of the navigation stack:
|
|
|
|
| Sheet | Trigger | Content |
|
|
|-------|---------|---------|
|
|
| UserPicker | Tap user avatar in nav bar | User list, settings shortcuts |
|
|
| [`NewChatView`](../../Shared/Views/NewChat/NewChatView.swift#L78) | Tap FAB / "+" button | Create link, scan QR, paste link, new group |
|
|
| WhatsNew | App update detected | Release notes |
|
|
| AddGroupView | "New Group" action | Group creation wizard |
|
|
| ConnectDesktopView | Settings > Desktop | Remote desktop pairing |
|
|
| MigrateFromDevice | Settings > Migration | Device export |
|
|
| MigrateToDevice | Onboarding migration | Device import |
|
|
| [LocalAuthView](../../Shared/ContentView.swift#L95) | App foreground after background | Biometric/passcode auth |
|
|
|
|
### Sheet Management
|
|
|
|
Sheets use SwiftUI `.sheet(item:)` or `.sheet(isPresented:)` modifiers on `ContentView` and `ChatListView`. Some sheets use the centralized [`AppSheetState.shared`](../../Shared/ContentView.swift#L29) observable for coordination:
|
|
|
|
```swift
|
|
class AppSheetState: ObservableObject {
|
|
static let shared = AppSheetState()
|
|
var scenePhaseActive: Bool = false
|
|
// ... sheet state coordination
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 5. Deep Linking
|
|
|
|
### Notification Deep Link
|
|
|
|
When the user taps a notification:
|
|
|
|
1. `NtfManager.processNotificationResponse()` extracts the `chatId` from notification payload
|
|
2. If a different user: calls `changeActiveUser(userId:)`
|
|
3. Sets `ChatModel.chatId = chatId` to navigate to the conversation
|
|
4. If the app was in background: the notification response is stored in `ChatModel.notificationResponse` and processed when the app becomes active
|
|
|
|
### [URL Deep Link](../../Shared/ContentView.swift#L281)
|
|
|
|
SimpleX links (`simplex:/chat#...`) are handled via [`connectViaUrl()`](../../Shared/ContentView.swift#L439):
|
|
|
|
```swift
|
|
.onOpenURL { url in
|
|
if AppChatState.shared.value == .active {
|
|
chatModel.appOpenUrl = url // Process immediately
|
|
} else {
|
|
chatModel.appOpenUrlLater = url // Process when active
|
|
}
|
|
}
|
|
```
|
|
|
|
URL processing routes to the appropriate connection flow (join group, add contact, etc.) via [`planAndConnect()`](../../Shared/Views/NewChat/NewChatView.swift#L1181).
|
|
|
|
### Call Deep Link
|
|
|
|
Call invitations from notifications:
|
|
1. `NtfManager` detects `ntfActionAcceptCall` action
|
|
2. Sets `ChatModel.ntfCallInvitationAction = (chatId, .accept)`
|
|
3. `ContentView` picks up the pending action and initiates the call
|
|
|
|
---
|
|
|
|
## 6. Call Overlay
|
|
|
|
The call UI overlays the entire app when a call is active:
|
|
|
|
### [Call Banner](../../Shared/ContentView.swift#L203)
|
|
|
|
When `ChatModel.activeCall != nil` and call is in connecting/active state:
|
|
- A banner appears at the top of ContentView (height: [`callTopPadding = 40`](../../Shared/ContentView.swift#L54))
|
|
- Shows contact name, call duration, tap to return to full-screen call
|
|
- Main content is padded down to accommodate the banner
|
|
|
|
### [Full-Screen Call View](../../Shared/ContentView.swift#L185)
|
|
|
|
When `ChatModel.showCallView == true`:
|
|
- `ActiveCallView` covers the entire screen as a ZStack overlay
|
|
- Contains local/remote video, controls (mute, camera, speaker, end)
|
|
- PiP mode: `ChatModel.activeCallViewIsCollapsed` collapses to mini view
|
|
- Call view is always rendered on top of navigation and sheets
|
|
|
|
```swift
|
|
// In ContentView.allViews():
|
|
ZStack {
|
|
contentView()
|
|
.padding(.top, showCallArea ? callTopPadding : 0)
|
|
|
|
if showCallArea, let call = chatModel.activeCall {
|
|
VStack {
|
|
activeCallInteractiveArea(call)
|
|
Spacer()
|
|
}
|
|
}
|
|
|
|
if chatModel.showCallView, let call = chatModel.activeCall {
|
|
callView(call) // Full screen overlay
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 7. Authentication Gate
|
|
|
|
### [Local Authentication](../../Shared/ContentView.swift#L359)
|
|
|
|
When [`DEFAULT_PERFORM_LA`](../../Shared/ContentView.swift#L44) is enabled:
|
|
|
|
1. App enters background: `chatModel.contentViewAccessAuthenticated = false`
|
|
2. App returns to foreground: `ContentView` shows [`lockButton()`](../../Shared/ContentView.swift#L238) instead of content
|
|
3. User taps lock button: [`LocalAuthView`](../../Shared/ContentView.swift#L95) presented
|
|
4. On successful auth: `chatModel.contentViewAccessAuthenticated = true`, content revealed
|
|
|
|
### Authentication Methods
|
|
- Face ID / Touch ID (via `LocalAuthentication` framework)
|
|
- Custom numeric passcode
|
|
- Custom alphanumeric passcode
|
|
|
|
### [Extended Authentication](../../Shared/ContentView.swift#L351)
|
|
- After successful auth, a grace period prevents re-auth for brief background/foreground cycles ([`unlockedRecently()`](../../Shared/ContentView.swift#L351))
|
|
- [`contentAccessAuthenticationExtended`](../../Shared/ContentView.swift#L35) is computed at `ContentView.init` to avoid render-time race conditions
|
|
- The `enteredBackgroundAuthenticated` timestamp tracks when the app was last authenticated in background
|
|
|
|
---
|
|
|
|
## 8. [Onboarding Flow](../../Shared/Views/Onboarding/OnboardingView.swift#L13)
|
|
|
|
First-launch experience controlled by [`ChatModel.onboardingStage`](../../Shared/Views/Onboarding/OnboardingView.swift#L46):
|
|
|
|
```swift
|
|
enum OnboardingStage: String, Identifiable {
|
|
case step1_SimpleXInfo // Welcome screen
|
|
case step2_CreateProfile // deprecated
|
|
case step3_CreateSimpleXAddress // deprecated
|
|
case step3_ChooseServerOperators // Choose server operators
|
|
case step4_SetNotificationsMode // Set notification preferences
|
|
case onboardingComplete // Normal operation
|
|
}
|
|
```
|
|
|
|
Each stage is a dedicated view presented in place of `ChatListView` within [`ContentView`](../../Shared/ContentView.swift#L174).
|
|
|
|
Migration state (`ChatModel.migrationState != nil`) takes precedence over onboarding.
|
|
|
|
---
|
|
|
|
## 9. Channel Creation Flow (`AddChannelView`)
|
|
|
|
**Source:** [`Shared/Views/NewChat/AddChannelView.swift`](../../Shared/Views/NewChat/AddChannelView.swift)
|
|
|
|
### Entry Point
|
|
|
|
`NewChatMenuButton` includes a NavigationLink "Create public channel" with antenna icon, navigating to `AddChannelView`.
|
|
|
|
### Three-Step Wizard
|
|
|
|
| Step | Function | Description |
|
|
|------|----------|-------------|
|
|
| 1. Profile | `profileStepView()` | Channel name input (`channelNameTextField()`), profile image picker. "Configure relays" link to `NetworkAndServers`. Validates via `canCreateProfile()` (non-empty + valid display name) and `checkHasRelays()`. |
|
|
| 2. Progress | `progressStepView(_:)` | Relay connection progress with `RelayProgressIndicator` (circular active/total or spinner). Expandable relay list with `relayStatusIndicator(_:)` (green/red/orange dots). Cancel via `cancelChannelCreation(_:)` which calls `apiDeleteChat`. |
|
|
| 3. Link | `linkStepView(_:)` | Wraps `GroupLinkView(isChannel: true)` for channel link sharing. |
|
|
|
|
### Key Functions
|
|
|
|
| Function | Scope | Description |
|
|
|----------|-------|-------------|
|
|
| `createChannel()` | private | Calls `apiNewPublicGroup(incognito:relayIds:groupProfile:)`, sets `ChannelRelaysModel` |
|
|
| `getEnabledRelays()` | private | Filters enabled/non-deleted relays, selects random 3 |
|
|
| `checkHasRelays()` | private | Validates at least one relay exists |
|
|
| `relayDisplayName(_:)` | module | name > domain > link host > fallback |
|
|
| `relayStatusIndicator(_:)` | module | Green/red/orange dot + status text |
|
|
| `RelayProgressIndicator` | module | Circular progress (active/total) or spinner |
|
|
|
|
## 10. Relay URL Interception
|
|
|
|
**Source:** [`Shared/ContentView.swift`](../../Shared/ContentView.swift#L454)
|
|
|
|
In `connectViaUrl_()`, relay address links (URL path `/r`) are intercepted before processing:
|
|
|
|
```swift
|
|
if path == "/r" {
|
|
showAlert(NSLocalizedString("Relay address", ...),
|
|
message: NSLocalizedString("This is a chat relay address, it cannot be used to connect.", ...))
|
|
return
|
|
}
|
|
```
|
|
|
|
Similarly, in `planAndConnect()` (`NewChatView.swift`), `.simplexLink(_, .relay, _, _)` patterns trigger the same alert and block connection.
|
|
|
|
## 11. Channel-Specific NewChatView Behavior
|
|
|
|
**Source:** [`Shared/Views/NewChat/NewChatView.swift`](../../Shared/Views/NewChat/NewChatView.swift)
|
|
|
|
### Prepared Group Alert (`showPrepareGroupAlert`)
|
|
|
|
When `groupShortLinkInfo?.direct == false` (channel relay link), the prepare alert uses:
|
|
- Channel icon: `antenna.radiowaves.left.and.right.circle.fill`
|
|
- Title: "Open new channel"
|
|
- Error: "Error opening channel"
|
|
- `apiPrepareGroup` call passes `directLink: false`
|
|
- Stores `groupShortLinkInfo.groupRelays` in `ChatModel.shared.channelRelayHostnames`
|
|
|
|
### Own Link Confirmation (`showOwnGroupLinkConfirmConnectSheet`)
|
|
|
|
For channels: shows "This is your link for channel" with only "Open channel" + "Cancel" buttons. No incognito or profile selection options.
|
|
|
|
### Known Group Alert (`showOpenKnownGroupAlert`)
|
|
|
|
For channels (`groupInfo.useRelays`): titles become "Open channel" / "Open new channel".
|
|
|
|
---
|
|
|
|
## Source Files
|
|
|
|
| File | Path |
|
|
|------|------|
|
|
| Root view | [`Shared/ContentView.swift`](../../Shared/ContentView.swift) |
|
|
| App entry point | `Shared/SimpleXApp.swift` |
|
|
| Navigation compat | [`Shared/Views/Helpers/NavStackCompat.swift`](../../Shared/Views/Helpers/NavStackCompat.swift) |
|
|
| Chat list (nav root) | `Shared/Views/ChatList/ChatListView.swift` |
|
|
| Nav link wrapper | `Shared/Views/ChatList/ChatListNavLink.swift` |
|
|
| User picker | `Shared/Views/ChatList/UserPicker.swift` |
|
|
| New chat view | [`Shared/Views/NewChat/NewChatView.swift`](../../Shared/Views/NewChat/NewChatView.swift) |
|
|
| Channel creation | [`Shared/Views/NewChat/AddChannelView.swift`](../../Shared/Views/NewChat/AddChannelView.swift) |
|
|
| New chat menu | [`Shared/Views/NewChat/NewChatMenuButton.swift`](../../Shared/Views/NewChat/NewChatMenuButton.swift) |
|
|
| Settings view | [`Shared/Views/UserSettings/SettingsView.swift`](../../Shared/Views/UserSettings/SettingsView.swift) |
|
|
| User profiles | [`Shared/Views/UserSettings/UserProfilesView.swift`](../../Shared/Views/UserSettings/UserProfilesView.swift) |
|
|
| Onboarding view | [`Shared/Views/Onboarding/OnboardingView.swift`](../../Shared/Views/Onboarding/OnboardingView.swift) |
|
|
| Active call view | `Shared/Views/Call/ActiveCallView.swift` |
|
|
| Local auth view | `Shared/Views/LocalAuth/LocalAuthView.swift` |
|
|
| Notification manager | `Shared/Model/NtfManager.swift` |
|