KSP library and Gradle plugin for generating ComposeUIViewController and UIViewControllerRepresentable files when using Compose Multiplatform for iOS.
| Version | Kotlin | KSP | Compose Multiplatform | Xcode |
|---|---|---|---|---|
2.3.0-1.10.0-rc02-4 |
2.3.0 | 2.3.4 | 1.10.0-rc02 | 26.2.0 |
As the project expands, the codebase required naturally grows, which can quickly become cumbersome and susceptible to errors. To mitigate this challenge, this library leverages Kotlin Symbol Processing to automatically generate the necessary Kotlin and Swift code for you.
It can be used for simple and advanced use cases.
In simple scenarios, the rendering and state management of the @Composable or UIViewController are handled entirely within a single platform
— either in the shared KMP module or directly in the iOS app. It’s simply a matter of embedding the component in one platform or the other, with
no cross-platform coordination required.
In advanced scenarios, rendering and state management are collaborative between platforms. Certain operations — such as emitting state updates or
embedding a @Composable or UIViewController — can be delegated from one platform to the other. This enables more complex integrations, where
state and UI responsibilities are shared or transferred as needed between the KMP module and the iOS app.
Kotlin Multiplatform and Compose Multiplatform are built upon the philosophy of incremental adoption and sharing only what you require. Consequently, the support for this specific use-case - in my opinion - is of paramount importance, especially in its capacity to entice iOS developers to embrace Compose Multiplatform.
Note
This library takes care of the heavy lifting for you, but if you're interested in understanding how it works, the detailed approach is explained here: Compose Multiplatform — Managing UI State on iOS.
Configure the plugins block with the following. Once added, you can use the ComposeUiViewController block to set up the plugin’s configuration.
plugins {
id("org.jetbrains.kotlin.multiplatform")
id("io.github.guilhe.kmp.plugin-composeuiviewcontroller") version "$LASTEST_VERSION"
}
ComposeUiViewController {
iosAppName = "Gradient"
targetName = "Gradient"
}With this setup, all necessary configurations are automatically applied. You only need to adjust the ComposeUiViewController block to match your
project settings (e.g. iosAppName and targetName). If you wish to change the default values, you can configure its parameters:
Parameters available
iosAppFolderNamename of the folder containing the iosApp in the root's project tree;iosAppNamename of the iOS project (name.xcodeproj);targetNamename of the iOS project's target;exportFolderNamename of the destination folder inside iOS project (iosAppFolderName) where theUIViewControllerRepresentablefiles will be copied to whenautoExportistrue;autoExportenables auto export generated files to Xcode project. If set tofalse, you will find the generated files under/build/generated/ksp/;
To enable Swift Export support, just follow the official documentation.
When using dependencies from other modules:
swiftExport {
export(projects.otherModule) { ... }
}Don't forget to import the plugin in each module. Check the swift export sample.
Important
When switching between modes - embedAndSignAppleFrameworkForXcode to embedSwiftExportForXcode or vice-versa - it's recommended to follow this
steps:
- Delete the
Derived Datausing Xcode or DevCleaner app; - Run
./gradlew clean --no-build-cache.
Inside iosMain we can take advantage of two annotations:
@ComposeUIViewController:
To annotate the @Composable as a desired ComposeUIViewController to be used by the iOS app.
@ComposeUIViewControllerState:
To annotate the parameter as the composable state variable (for advanced use cases).
Important
Only 0 or 1 @ComposeUIViewControllerState and an arbitrary number of parameter types (excluding @Composable) are allowed in @ComposeUIViewController functions.
Simple
@ComposeUIViewController
@Composable
internal fun ComposeSimpleView() { }will produce a ComposeSimpleViewUIViewController:
object ComposeSimpleViewUIViewController {
fun make(): UIViewController {
return ComposeUIViewController {
ComposeSimpleView()
}
}
}and also a ComposeSimpleViewRepresentable:
import Shared
import SwiftUI
public struct ComposeSimpleViewRepresentable: UIViewControllerRepresentable {
func makeUIViewController(context _: Context) -> UIViewController {
ComposeSimpleViewUIViewController().make()
}
func updateUIViewController(_: UIViewController, context _: Context) {
// unused
}
}Advanced
data class ViewState(val isLoading: Boolean)
@ComposeUIViewController
@Composable
internal fun ComposeAdvancedView(@ComposeUIViewControllerState viewState: ViewState, callback: () -> Unit) { }will produce a ComposeAdvancedViewUIViewController:
object ComposeAdvancedViewUIViewController {
private val viewState = mutableStateOf<ViewState?>(null)
fun make(callback: () -> Unit): UIViewController {
return ComposeUIViewController {
viewState.value?.let { ComposeAdvancedView(it, callback) }
}
}
fun update(viewState: ViewState) {
this.viewState.value = uiState
}
}and also a ComposeAdvancedViewRepresentable:
import Shared
import SwiftUI
public struct ComposeAdvancedViewRepresentable: UIViewControllerRepresentable {
@Binding var viewState: ViewState
let callback: () -> Void
func makeUIViewController(context _: Context) -> UIViewController {
ComposeAdvancedViewUIViewController().make(callback: callback)
}
func updateUIViewController(_: UIViewController, context _: Context) {
ComposeAdvancedViewUIViewController().update(viewState: viewState)
}
}After a successful build the UIViewControllerRepresentable files are included and referenced in the xcodeproj ready to be used:
import SwiftUI
import Shared
struct SomeView: View {
@State private var state: ViewState = ViewState(isLoading: false)
var body: some View {
VStack {
ComposeSimpleViewRepresentable()
ComposeAdvancedViewRepresentable(viewState: $state, callback: {})
}
}
}Important
Avoid deleting iosApp/Representables without using Xcode.
For a working sample open iosApp/Gradient.xcodeproj in Xcode and run standard configuration or use KMP plugin for Android Studio and choose iosApp in run configurations.
You'll find tree scenarios demonstrating the different use cases:
GradientScreenCompose: A screen rendered entirely in Compose with its state controlled by iOS;GradientScreenMixed: A screen rendered in Compose with a Swift UIViewController embedded in it;GradientScreenSwift: A screen rendered entirely in Swift and embedded in Compose.
You can also find other working samples in:
When building the KMP module, you should see output similar to this:
> Task :shared:copyFilesToXcode
> Starting smart sync process
> New file: GradientScreenSwiftUIViewControllerRepresentable.swift
> New file: GradientScreenMixedUIViewControllerRepresentable.swift
> New file: GradientScreenComposeUIViewControllerRepresentable.swift
> Summary: 0 unchanged, 3 copied, 0 removed
> Detected changes. Rebuilding Xcode references
> Created new group "Representables"
> Adding: GradientScreenComposeUIViewControllerRepresentable.swift
> Adding: GradientScreenMixedUIViewControllerRepresentable.swift
> Adding: GradientScreenSwiftUIViewControllerRepresentable.swift
> Summary: 3 added, 0 removed, 0 unchanged
> DoneCopyright (c) 2023-present GuilhE
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.