Skip to main content

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:

  1. View dispatches an Action to the Store
  2. Reducer (pure function) produces a new immutable State
  3. For async work, the Store calls a Repository
  4. Repository coordinates one or more Services (Apollo, Firebase, Stripe…)
  5. 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.

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

FeatureStatusHighlights
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

ComponentDescription
DIContainerThread-safe dependency injection container
CacheCoordinatorUnified cache with TTL + eviction: .memory 10MB, .images 50MB, .disk 100MB
ErrorHandlerCentralized error handling with automatic retry, re-auth support, user notifications
NetworkMonitorReal-time connectivity via NWPathMonitor + Combine publishers
MessageQueuePersistent offline message queue with exponential backoff retry (max 3)
LoggerUnified logging system

External services (iOS)

ServicePurpose
FirebaseAuth (phone, Google), Analytics, Crashlytics
LiveKitWebRTC audio/video calls
StripePayment sheet integration
Google Sign-InOAuth login
Apple Sign InNative auth
GiphyGIF/sticker picker in chat
CallKitNative iOS call UI
VoIP pushBackground 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 OfflineBanner with 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.