The journey of using compose Multi-Platform for iOS.

Prasanth Perumal
4 min readMar 10, 2023

There is no proper documentation for adding Jetpack Compose for iOS because it is still in early development stages. Its not intended to be used in production . But developers need to understand the awesomeness of it. Early adopters like touchlab , joreilly and many more developers have started experimenting with it and have created side projects with it. But the integration is no simple task. Laden with lots of gradle related confusion the initial setup can be tough.

But Jetbrains team has provided us with a template which we can use to start. Its not discussed much but the template is given in https://github.com/JetBrains/compose-multiplatform-template

clone the above template and you can run the app in simulator using android studio. (You must have installed xcode command line tools for this to work). In case you are planning to add compose for iOS support to an existing kmm app then do the following code changes.

  1. In the Settings.gradle file add
pluginManagement {
repositories {
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
}
plugins {
id("org.jetbrains.compose").version("1.4.0-alpha01-dev954")
}
}

2. In the application build.gradle add

plugins {
id("org.jetbrains.compose") apply false
}
allprojects {
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
}

3. In the gradle.properties file add the experimental settings

org.jetbrains.compose.experimental.uikit.enabled=true
kotlin.native.cacheKind=none

4. In the module level build.gradle file add under plugins and sourcesets

plugins {
id("org.jetbrains.compose")
}
cocoapods {
summary = "Some description for the Shared Module"
homepage = "Link to the Shared Module homepage"
version = "1.0"
ios.deploymentTarget = "14.1"
podfile = project.file("../iosApp/Podfile")
framework {
baseName = "shared"
// isStatic is very important and needs to be added
isStatic = true
}
}

sourceSets {
val commonMain by getting {
dependencies {
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material)
@OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class)
implementation(compose.components.resources)
}
}

Note : you can add dependsOn(commonMain) to your iosMain Sourceset incase you are creating a expect function and the actual implementation is inside iosMain package

5. sync the gradle and it should complete successfully

6. In your compose code import

import androidx.compose.ui.window.ComposeUIViewController

and create a UIViewController using ComposeViewController inside which you can call your composable functions directly

// Main.kt 
fun composeApplication() = ComposeUIViewController {
// call your composables here
Column{

}
}

7. To use this in swiftUI you need to create a UIControllerRepresentable

create a MainUIControllerRepresentable.swift file and call the ComposeUIController using the kotlin filename

//MainUIControllerRepresentable.swift
struct MainUIControllerRepresentable: UIViewControllerRepresentable {

func makeUIViewController(context: Context) -> some UIViewController {
Mainkt.composeApplication()
}

func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
uiViewController.view.setNeedsLayout()
}
}
//iOSApp.swift
@main
struct iOSApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}

//ContentView.swift
struct ContentView: View {
var body: some View {
MainUIControllerRepresentable()
.ignoresSafeArea(.keyboard) // Compose has own keyboard handler
}
}

8. Run the application using android studio and you will be able to see the compose screen in your simulator.

I have created a repository from my journey and its in

This is the flow from the official sample but we can make this still way cooler. Do you know we can code the project completely in kotlin and allow gradle to create the iOS project while compiling . Wouldn't that be awesome?

joreilly in his open source repo has done it

The changes needed for this is

  1. create a Uikit task in gradle i.e build.gradle in module.
compose.experimental {
uikit.application {
bundleIdPrefix = "prasanth.iosApp.compose"
projectName = "ComposeMultiPlatApp"
deployConfigurations {
simulator("IPhone13Pro") {
//Usage: ./gradlew iosDeployIPhone13ProDebug
device = org.jetbrains.compose.experimental.dsl.IOSDevices.IPHONE_13_PRO
}
connectedDevice("Device") {
//Usage: ./gradlew iosDeployDeviceRelease
this.teamId="..."
}
}
}
}

2. add the entry point as main function in sourceset . as we are using uikit we need to name iosX64 as uikitX64 and iosArm64 as uikitArm64

iosX64("uikitX64") {
binaries {
executable() {
entryPoint = "main"
freeCompilerArgs += listOf(
"-linker-option", "-framework", "-linker-option", "Metal",
"-linker-option", "-framework", "-linker-option", "CoreText",
"-linker-option", "-framework", "-linker-option", "CoreGraphics"
)
}
}
}
iosArm64("uikitArm64") {
binaries {
executable() {
entryPoint = "main"
freeCompilerArgs += listOf(
"-linker-option", "-framework", "-linker-option", "Metal",
"-linker-option", "-framework", "-linker-option", "CoreText",
"-linker-option", "-framework", "-linker-option", "CoreGraphics"
)
}
}
}
sourceSets {
val uikitX64Main by getting{
dependsOn(iosMain)
}
val uikitArm64Main by getting{
dependsOn(iosMain)
}
}

3) create a main.kt file with the main function in src/kotlin folder with

public fun main() {
val args = emptyArray<String>()
memScoped {
val argc = args.size + 1
val argv = (arrayOf("myApp") + args).map { it.cstr.ptr }.toCValues()
autoreleasepool {
UIApplicationMain(argc, argv, null, NSStringFromClass(MainAppDelegate))
}
}
}

class MainAppDelegate @ObjCObjectBase.OverrideInit constructor() : UIResponder(), UIApplicationDelegateProtocol {
companion object : UIResponderMeta(), UIApplicationDelegateProtocolMeta

private var _window: UIWindow? = null
override fun window() = _window
override fun setWindow(window: UIWindow?) {
_window = window
}
override fun application(
application: UIApplication,
didFinishLaunchingWithOptions: Map<Any?, *>?
): Boolean {
window = UIWindow(frame = UIScreen.mainScreen.bounds)
// Application is an extension function which returns a UIViewController
window?.rootViewController = Application {
// Add your Composables here
Column{
}
}
window?.makeKeyAndVisible()
return true
}
}

4. Run ./gradlew iosDeployIPhone13ProDebug and it will launch the simulator.

The gradle creates an iOS project in the build folder which you can use in xcode.

Thats all Folks .Compose for iOS is a big leap in hybrid applications and lots of hype is going around. This blog is for setting up Compose for iOS and my journey in integrating it. kindly support by clicking on the clap icon.

--

--