Skip to main content

iOS Platform

Build and deploy GreenhouseAdmin as a native iOS application. This guide covers Xcode setup, framework generation, and iOS-specific configuration.

Overview

The iOS platform uses:
  • Framework: ComposeApp (static framework)
  • Targets: iosArm64 (devices), iosSimulatorArm64 (M1+ simulators)
  • HTTP Engine: Darwin (native URLSession)
  • Minimum iOS: 14.0+
  • UI Integration: SwiftUI wrapper around Compose Multiplatform
iOS requires macOS and Xcode for development and building.

Prerequisites

1

Install Xcode

Download and install Xcode from the Mac App Store (Xcode 15.0+).Verify installation:
xcodebuild -version
2

Install Command Line Tools

Install Xcode command-line tools:
xcode-select --install
3

Accept Xcode License

Accept the Xcode license agreement:
sudo xcodebuild -license accept
4

Install CocoaPods (Optional)

For dependency management (if needed):
sudo gem install cocoapods

Configuration

iOS Framework Setup

The Kotlin Multiplatform code is compiled into a static iOS framework:
composeApp/build.gradle.kts
listOf(
    iosArm64(),        // Physical devices
    iosSimulatorArm64() // M1/M2/M3 Mac simulators
).forEach { iosTarget ->
    iosTarget.binaries.framework {
        baseName = "ComposeApp"
        isStatic = true
    }
}

iOS-Specific Dependencies

The iOS platform uses the Darwin engine for Ktor networking:
composeApp/build.gradle.kts
iosMain.dependencies {
    // Ktor - Darwin engine for iOS (uses URLSession)
    implementation(libs.ktor.client.darwin)
}

Dependency Injection (Koin)

iOS requires platform-specific initialization in the SwiftUI app:
iosApp/iOSApp.swift
import SwiftUI
import ComposeApp

@main
struct iOSApp: App {
    init() {
        MainViewControllerKt.doInitKoin()
    }

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}
The doInitKoin() function is exposed from Kotlin:
iosMain/.../MainViewController.kt
fun doInitKoin() {
    initKoin()
}

Development

Running on Simulator

1

Open iOS Project

Open the iosApp directory in Xcode:
open iosApp/iosApp.xcodeproj
2

Select Simulator

Choose a simulator from the device dropdown (e.g., iPhone 15 Pro).
3

Build and Run

Click the Run button (⌘R) or use Product > Run.
The first build may take several minutes as Gradle compiles the Kotlin framework.

Running on Physical Device

1

Configure Signing

In Xcode:
  1. Select the iosApp target
  2. Go to Signing & Capabilities
  3. Select your Team
  4. Ensure Automatically manage signing is enabled
2

Generate Device Framework

Build the framework for physical devices:
./gradlew :composeApp:linkDebugFrameworkIosArm64
3

Connect Device

Connect your iPhone/iPad via USB and select it in Xcode’s device dropdown.
4

Build and Run

Click the Run button (⌘R) in Xcode.
On first run, you may need to trust the developer certificate on the device: Settings > General > VPN & Device Management

Viewing Logs

Monitor iOS logs:
Logs appear in the bottom panel: View > Debug Area > Activate Console (⇧⌘C)

Building for Distribution

Archive Build

1

Select Generic iOS Device

In Xcode, select Any iOS Device (arm64) from the device dropdown.
2

Create Archive

Go to Product > Archive (or ⇧⌘B for build only).This creates an archive in the Xcode Organizer.
3

Distribute Archive

In the Organizer:
  1. Select the archive
  2. Click Distribute App
  3. Choose distribution method:
    • App Store Connect: For App Store release
    • Ad Hoc: For internal testing (up to 100 devices)
    • Enterprise: For enterprise distribution
    • Development: For development testing

Command-Line Archive

1

Build Release Framework

Generate the release framework:
./gradlew :composeApp:linkReleaseFrameworkIosArm64
2

Create Archive

Build and archive:
cd iosApp
xcodebuild archive \
  -project iosApp.xcodeproj \
  -scheme iosApp \
  -sdk iphoneos \
  -configuration Release \
  -archivePath build/iosApp.xcarchive
3

Export IPA

Export the IPA file:
xcodebuild -exportArchive \
  -archivePath build/iosApp.xcarchive \
  -exportPath build/iosApp \
  -exportOptionsPlist ExportOptions.plist
Create ExportOptions.plist with your distribution method:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>method</key>
    <string>app-store</string>
    <key>teamID</key>
    <string>YOUR_TEAM_ID</string>
</dict>
</plist>

SwiftUI Integration

The iOS app wraps the Compose Multiplatform UI in SwiftUI:
iosApp/ContentView.swift
import SwiftUI
import ComposeApp

struct ContentView: View {
    var body: some View {
        ComposeView()
            .ignoresSafeArea(.all)
    }
}

struct ComposeView: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> UIViewController {
        return MainViewControllerKt.createMainViewController()
    }
    
    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}
Expose the Compose UI from Kotlin:
iosMain/.../MainViewController.kt
fun createMainViewController(): UIViewController {
    return ComposeUIViewController {
        App()  // Shared Compose UI from commonMain
    }
}

iOS-Specific Features

Safe Area Handling

Handle iOS safe areas in Compose:
iosMain/.../SafeAreaInsets.kt
@Composable
actual fun getSystemInsets(): WindowInsets {
    val safeArea = LocalSafeArea.current
    return WindowInsets(
        left = safeArea.left.dp,
        top = safeArea.top.dp,
        right = safeArea.right.dp,
        bottom = safeArea.bottom.dp
    )
}

Native iOS APIs

Access iOS-specific APIs using expect/actual:
iosMain/.../PlatformUtils.kt
import platform.UIKit.UIDevice
import platform.Foundation.NSUUID

actual fun getDeviceId(): String {
    return UIDevice.currentDevice.identifierForVendor?.UUIDString ?: ""
}

actual fun shareText(text: String) {
    val activityViewController = UIActivityViewController(
        activityItems = listOf(text),
        applicationActivities = null
    )
    // Present activity view controller
}

Info.plist Configuration

Configure app permissions and settings:
iosApp/Info.plist
<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <false/>
    <key>NSExceptionDomains</key>
    <dict>
        <key>api.example.com</key>
        <dict>
            <key>NSExceptionAllowsInsecureHTTPLoads</key>
            <false/>
            <key>NSIncludesSubdomains</key>
            <true/>
        </dict>
    </dict>
</dict>

<key>UIRequiredDeviceCapabilities</key>
<array>
    <string>arm64</string>
</array>

Testing

TestFlight Distribution

1

Archive for App Store

Create an App Store archive as described above.
2

Upload to App Store Connect

In Xcode Organizer:
  1. Select the archive
  2. Click Distribute App
  3. Choose App Store Connect
  4. Follow the prompts to upload
3

Configure TestFlight

In App Store Connect:
  1. Go to TestFlight tab
  2. Add internal/external testers
  3. Submit for beta review (external testers only)

Simulator Testing

Test on multiple simulators:
# List available simulators
xcrun simctl list devices

# Create a new simulator
xcrun simctl create "iPhone 15 Pro" "iPhone 15 Pro" "iOS17.0"

# Boot simulator
xcrun simctl boot "iPhone 15 Pro"

# Install and launch
xcrun simctl install booted path/to/app
xcrun simctl launch booted com.apptolast.greenhouse.admin

Troubleshooting

Error: ld: framework not found ComposeAppSolution: Build the iOS framework first:
./gradlew :composeApp:linkDebugFrameworkIosSimulatorArm64
Or in Xcode, add a Run Script phase:
cd "$SRCROOT/.."
./gradlew :composeApp:embedAndSignAppleFrameworkForXcode
Error: No signing certificate foundSolution:
  1. Ensure you’re logged into Xcode with your Apple ID
  2. Go to Xcode > Settings > Accounts
  3. Select your team and click Manage Certificates
  4. Create a new certificate if needed
Error: SSL error with Darwin clientSolution: Check App Transport Security settings in Info.plist. For development, you may need to allow insecure loads for specific domains.
Error: Need to run on Intel MacSolution: Add iosX64() target to build.gradle.kts:
listOf(
    iosArm64(),
    iosSimulatorArm64(),
    iosX64()  // For Intel Mac simulators
).forEach { ... }
Error: App crashes on launch with Koin errorsSolution: Ensure doInitKoin() is called in the SwiftUI app’s init():
@main
struct iOSApp: App {
    init() {
        MainViewControllerKt.doInitKoin()
    }
    ...
}

Next Steps

Android Platform

Build and deploy to Android devices

Web Platform

Deploy as a web application

Multiplatform

Share code between platforms

Architecture

MVVM architecture and patterns

Build docs developers (and LLMs) love