- Replace hardcoded simulator framework path with SDK-conditional paths so Xcode automatically resolves iosSimulatorArm64 or iosArm64 based on the selected build target - Fix Compose Resources build script to use \$PLATFORM_NAME instead of \$CONFIGURATION for platform detection - Add CODE_SIGN_IDENTITY override for iphoneos SDK to enable device signing - Document simulator and physical device build workflows in README
96 lines
4.4 KiB
Markdown
96 lines
4.4 KiB
Markdown
# LibreChat iOS App
|
|
|
|
Compose Multiplatform iOS client for LibreChat. The iOS app is a thin SwiftUI wrapper around the full Compose Multiplatform UI — all screens, navigation, and business logic are shared with Android via KMP.
|
|
|
|
## Architecture
|
|
|
|
The entire UI is rendered by Compose Multiplatform. SwiftUI is only used as a hosting layer:
|
|
|
|
```
|
|
iOSApp.swift — App entry point: initializes Koin DI, renders LibreChatComposeView
|
|
ComposeView.swift — UIViewControllerRepresentable wrapping MainViewController() (CMP root)
|
|
KoinHelper.swift — Swift-side Koin dependency resolver (for debugging / Swift-native code)
|
|
SharedFrameworkTest.swift — Compile-time smoke test for KMP + SKIE bridging
|
|
```
|
|
|
|
The `:shared` Gradle module exports `core:common`, `core:model`, `core:network`, and `core:data` as a single `Shared.framework`. All feature modules and `core:ui` are included for Compose Multiplatform screen sharing.
|
|
|
|
### Key components
|
|
|
|
- **SKIE**: Enhances Kotlin-Swift interop automatically — sealed classes become Swift enums (`onEnum(of:)`), `Flow<T>` becomes `AsyncSequence`, `suspend fun` becomes `async throws`
|
|
- **DI**: Koin is initialized in `iOSApp.init()` via `IosKoinHelperKt.startIosKoin()`
|
|
- **Crash Reporting**: `installCrashReporting()` sets up a Kotlin/Native unhandled exception hook that logs via Kermit + NSLog and raises as NSException
|
|
- **Platform Impls**:
|
|
- `IosTokenDataStore` (`core/data/src/iosMain/`) — Keychain-backed token storage via Security.framework
|
|
- `IosConnectivityObserver` (`core/common/src/iosMain/`) — NWPathMonitor via `nw_path_monitor_*` APIs
|
|
- `IosSharedModule` (`shared/src/iosMain/`) — Koin module wiring Darwin Ktor engine + all platform deps
|
|
|
|
## Building
|
|
|
|
### Prerequisites
|
|
- Apple Silicon Mac (Intel Macs are not supported — no `iosSimulatorX64` target)
|
|
- Xcode 15.0+ with iOS 16.0+ SDK (for toolchain and simulators — IDE not needed)
|
|
- JDK 17+ (for Gradle/Kotlin compilation)
|
|
- Android Studio or IntelliJ IDEA (recommended IDE for all code editing)
|
|
|
|
### Build and Run
|
|
|
|
#### Simulator
|
|
|
|
```bash
|
|
# 1. Build the shared KMP framework
|
|
./gradlew :shared:linkDebugFrameworkIosSimulatorArm64
|
|
|
|
# 2. Build the Xcode project
|
|
xcodebuild -project iosApp/iosApp.xcodeproj -scheme iosApp \
|
|
-sdk iphonesimulator \
|
|
-destination 'platform=iOS Simulator,name=iPhone 16' \
|
|
-derivedDataPath iosApp/build build
|
|
|
|
# 3. Boot simulator, install, and launch
|
|
xcrun simctl boot "iPhone 16"
|
|
xcrun simctl install booted iosApp/build/Build/Products/Debug-iphonesimulator/iosApp.app
|
|
xcrun simctl launch booted com.garfiec.librechat.ios
|
|
```
|
|
|
|
#### Physical Device
|
|
|
|
```bash
|
|
# 1. Build the shared KMP framework for device
|
|
./gradlew :shared:linkDebugFrameworkIosArm64
|
|
```
|
|
|
|
Then open the project in Xcode:
|
|
|
|
```bash
|
|
open iosApp/iosApp.xcodeproj
|
|
```
|
|
|
|
In Xcode:
|
|
1. Select your iPhone from the device picker (top toolbar)
|
|
2. Go to **Signing & Capabilities** → set your **Team** (required for device signing)
|
|
3. Press **⌘R** to build and install
|
|
|
|
> **Note:** The first build takes several minutes while the Kotlin/Native toolchain downloads. You need an Apple Developer account (free tier works) for device signing.
|
|
|
|
### Known Xcode Behaviour
|
|
|
|
**Red dot on `Shared.framework` in the navigator** — You may see a red indicator on `Shared.framework` in the Xcode file navigator (left sidebar). This is cosmetic and does not affect builds. It happens because the Xcode project file keeps a static reference path to the framework for display purposes, and that path may not exist if you haven't built that particular target yet. The linker always resolves the framework via `FRAMEWORK_SEARCH_PATHS`, which is set correctly per target — the red dot can be safely ignored. Building the Gradle framework for your active target clears it:
|
|
|
|
```bash
|
|
./gradlew :shared:linkDebugFrameworkIosSimulatorArm64 # clears it for simulator
|
|
./gradlew :shared:linkDebugFrameworkIosArm64 # clears it for device
|
|
```
|
|
|
|
## Info.plist Permissions
|
|
|
|
The following keys are configured in `Info.plist`:
|
|
- `NSMicrophoneUsageDescription` — Microphone for voice input
|
|
- `NSSpeechRecognitionUsageDescription` — Speech-to-text
|
|
- `NSCameraUsageDescription` — Camera for photo capture
|
|
- `NSPhotoLibraryUsageDescription` — Photo library access
|
|
- `CADisableMinimumFrameDurationOnPhone` — 120Hz ProMotion support
|
|
|
|
## URL Scheme
|
|
|
|
The app registers the `librechat://` URL scheme for deep linking (conversations, OAuth callbacks), matching the Android app's behavior.
|