iOS App
The Closegram iOS app is a production-ready SwiftUI application located at apps/ios/. It targets iOS 15+ and requires Xcode 15+.
Architecture: MVVM + Redux + Clean Architecture
Each feature is organized into three layers:
Feature/
├── Data/ → Repository implementations, Services
├── Domain/ → Business models, protocols
└── Presentation/ → Redux Store, ViewModels (UI only), Views
Unidirectional data flow:
- View dispatches an
Actionto theStore Reducer(pure function) produces a new immutableState- For async work, the
Storecalls aRepository - Repository coordinates one or more
Services(Apollo, Firebase, Stripe…) - Store updates state → SwiftUI re-renders
Dependency injection
Services and repositories are injected via a thread-safe DIContainer:
@Injected private var authRepository: AuthRepositoryProtocol
Everything is registered once in appApp.swift on startup.
Navigation
Pure SwiftUI, no UIKit bridge: MainTabView is a NavigationStack(path: $router.path) wrapping a TabView, with each of the 5 tabs (Updates/Calls/Communities/Chats/Settings) getting its own inner NavigationStack for per-tab push navigation, plus .navigationDestination(for: AppRoute.self) for the 4 cross-tab routes (.chat, .contactInfo, .archivedChats, .userProfile) driven by AppRouter. Because it's pure SwiftUI, the environment (all @EnvironmentObject stores) propagates automatically down the tree — there's no manual per-route re-attachment list to keep in sync, which was the recurring source of crashes in the previous UIViewControllerRepresentable-based implementation.
Feature modules
| Feature | Status | Highlights |
|---|---|---|
| Auth | ✅ 95% | Phone OTP, Apple Sign In, Google Sign In, password reset — token refresh and delete-account are the remaining gaps, see below |
| Chat | ✅ 100% | Real-time subscriptions, location sharing, reactions, group chats, typing indicators, GIF picker |
| Calls | ✅ 100% | LiveKit WebRTC, CallKit, Picture-in-Picture, speaker/viewer roles |
| Search | ✅ 100% | Debounced (500ms), user + conversation search, online/verified filters |
| Profile | ✅ 100% | View/edit own and others' profiles, upload photo/cover |
| Payment | ✅ 90% | Stripe integration, add/remove/default payment methods |
| CoinPackage | ✅ 100% | Browse and purchase coin packages |
Known TODOs
Critical (Chat): media uploads (images, audio, documents, polls), message edit/forward.
Critical (Auth): token refresh (AuthService.refreshToken() is a literal TODO stub), delete account (AuthRepository.deleteAccount() calls a real backend mutation, but the Settings-side ProfileRepository.deleteAccount() that the UI actually dispatches to is a stub throwing ProfileError.featureNotSupported, and neither path has an actual button/view wired to it).
Medium: message search (backend GraphQL pending), payment local caching.
Not implemented: Dark Mode, iPad support, VoiceOver/Dynamic Type accessibility.
Core infrastructure
| Component | Description |
|---|---|
DIContainer | Thread-safe dependency injection container |
CacheCoordinator | Unified cache with TTL + eviction: .memory 10MB, .images 50MB, .disk 100MB |
ErrorHandler | Centralized error handling with automatic retry, re-auth support, user notifications |
NetworkMonitor | Real-time connectivity via NWPathMonitor + Combine publishers |
MessageQueue | Persistent offline message queue with exponential backoff retry (max 3) |
Logger | Unified logging system |
External services (iOS)
| Service | Purpose |
|---|---|
| Firebase | Auth (phone, Google), Analytics, Crashlytics |
| LiveKit | WebRTC audio/video calls |
| Stripe | Payment sheet integration |
| Google Sign-In | OAuth login |
| Apple Sign In | Native auth |
| Giphy | GIF/sticker picker in chat |
| CallKit | Native iOS call UI |
| VoIP push | Background call alerts |
Networking
GraphQL queries, mutations, and subscriptions run via Apollo iOS. The generated Swift types live in the ClosegramGraphQL module (SwiftPM package packages/apollo-swift, generated from packages/graphql via npm run codegen:ios). Cache strategy is adaptive — online uses .returnCacheDataAndFetch, offline uses .returnCacheDataDontFetch.
Offline support (97% complete)
Users can:
- Send messages while offline (queued, auto-retried on reconnect)
- View cached conversations and messages
- View cached payment methods
- See a visual
OfflineBannerwith three states: offline → reconnecting → connected
Localization
Type-safe i18n with runtime language switching (no restart needed):
Text(LocalizedString.Chat.Message.send.localized)
LocalizedString.Chat.Group.participants(count: 5) // "5 participants"
LocalizationManager.shared.setLanguage(.spanish)
Languages: English (default), Spanish — 230+ keys across 11 categories.
Setup
cd apps/ios/app
open app.xcodeproj
# File → Packages → Resolve Package Versions
Set environment variables in the scheme (Run → Arguments):
STRIPE_PUBLISHABLE_KEY = pk_test_...
GIPHY_API_KEY = ...
API_HOST = <your machine's LAN IP> # local backend, physical device only
API_ENVIRONMENT = development # optional: point Debug builds at dev.api.closegram.com instead of local
Which of the three backend environments (local / development / production) a build talks to, and how to target each one from Xcode and Fastlane, is covered in Deployment Environments.
Regenerating GraphQL code
Whenever packages/graphql/schema/schema.web.graphqls or any .graphql operation under packages/graphql/operations/Web/** changes:
npm run codegen:schema # regenerate the schema apollo-swift codegens against
npm run codegen:ios # regenerate packages/apollo-swift/Sources/ClosegramGraphQL
then rebuild. CI enforces this — the test job in .github/workflows/ios-ci.yml runs swift run codegen (from packages/apollo-swift) and fails the build if the generated Swift is out of date relative to what's committed, so stale generated code can't merge silently.
Running tests
# Via Fastlane
SKIP_GIT_CHECK=true bundle exec fastlane test
# Via xcodebuild
xcodebuild test -scheme app -destination 'platform=iOS Simulator,name=iPhone 15 Pro'
Runs on every push/PR via .github/workflows/ios-ci.yml (macOS runner, Xcode 26.1 — required by this project's IPHONEOS_DEPLOYMENT_TARGET = 26.1). Test coverage today is concentrated entirely in AuthTests (9 files) — reducer, store and use-case tests for login/register/password flows, all passing in CI. No other feature has test coverage yet; see iOS Parity Migration.