KMP-ComposeUIViewController
KMP-ComposeUIViewController copied to clipboard
KSP library and Gradle Plugin for generating ComposeUIViewController and UIViewControllerRepresentable files when using Compose Multiplatform for iOS
KMP-ComposeUIViewController
KSP library for generating ComposeUIViewController
and UIViewControllerRepresentable
implementations when using Compose Multiplatform for iOS.
Motivation
When employing Compose Multiplatform for iOS, if the goal is to effectively manage the UI state within the iOS app, it's essential to adopt the approach detailed here: Compose Multiplatform — Managing UI State on iOS.
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 code for you.
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.
Compatibility
Version | Kotlin | KSP | K2 | Compose Multiplatform | Xcode |
---|---|---|---|---|---|
2.0.0 | 1.0.21 | Yes | 1.6.10 | 15.3.0 |
The suffix -ALPHA
and -BETA
will be added to reflect JetBrain's Compose Multiplatform iOS stability level, until it becomes STABLE
.
It's important to note that this addresses the current Compose Multiplatform API design. Depending on JetBrains' future implementations, this may potentially become deprecated.
Configurations
Step 1 - Setup code generation
KMP shared module
Gradle
First we need to import the ksp plugin:
plugins {
id("com.google.devtools.ksp") version "${Kotlin}-${KSP}"
}
Then configure iosMain target to import kmp-composeuiviewcontroller-annotations
:
kotlin {
sourceSets {
iosMain.dependencies {
implementation("com.github.guilhe.kmp:kmp-composeuiviewcontroller-annotations:${LASTEST_VERSION}")
}
}
}
and also the kmp-composeuiviewcontroller-ksp
:
listOf(iosArm64(), iosSimulatorArm64(), iosX64()).forEach { target ->
val targetName = target.name.replaceFirstChar { it.uppercaseChar() }
dependencies.add("ksp$targetName", "com.github.guilhe.kmp:kmp-composeuiviewcontroller-ksp:${LASTEST_VERSION}")
}
Finish it by adding this task
configuration in the end of the file:
- If using XCFramework:
tasks.matching { it.name == "embedAndSignAppleFrameworkForXcode" }.configureEach { finalizedBy(":addFilesToXcodeproj") }
- If using Cocoapods:
tasks.matching { it.name == "syncFramework" }.configureEach { finalizedBy(":addFilesToXcodeproj") }
You can find a full setup example here.
Code generation
Now we can take advantage of two annotations:
-
@ComposeUIViewController
: it will mark the@Composable
as a desiredComposeUIViewController
to be used by the iosApp; -
@ComposeUIViewControllerState
: it will specify the composable state variable.
Rules and considerations
-
@ComposeUIViewController
will always require a unique@ComposeUIViewControllerState
; -
@ComposeUIViewController
has aframeworkName
parameter that must be used to specify the shared library framework's base name; -
@ComposeUIViewControllerState
can only be applied once per@Composable
; - The state variable of your choosing must have default values in it's initialization;
- Only 1
@ComposeUIViewControllerState
and * function parameters (excluding@Composable
) are allowed in@ComposeUIViewController
functions.
For more information consult the ProcessorTest.kt file from kmp-composeuiviewcontroller-ksp
.
Example
data class ViewState(val status: String = "default")
@ComposeUIViewController("SharedUI")
@Composable
fun ComposeView(@ComposeUIViewControllerState viewState: ViewState, callback: () -> Unit) { }
will produce a ComposeViewUIViewController
:
object ComposeViewUIViewController {
private val viewState = mutableStateOf(ViewState())
fun make(callback: () -> Unit): UIViewController {
return ComposeUIViewController {
ComposeView(viewState.value, callback)
}
}
fun update(viewState: ViewState) {
this.viewState.value = uiState
}
}
and also a ComposeViewRepresentable
:
import SwiftUI
import SharedUI
public struct ComposeViewRepresentable: UIViewControllerRepresentable {
@Binding var viewState: ScreenState
let callback: () -> Void
func makeUIViewController(context: Context) -> UIViewController {
ComposeViewUIViewController().make(callback: callback)
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
ComposeViewUIViewController().update(viewState: viewState)
}
}
Step 2 - Setup auto export to Xcode
Project root
Having all the files created by KSP, the next step is to make sure all the UIViewControllerRepresentable
files are referenced in xcodeproj
for the desire target
:
- Make sure you have Xcodeproj installed;
- Copy the exportToXcode.sh file to the project's root and run
chmod +x ./exportToXcode.sh
- Copy the following gradle task to the project's root
build.gradle.kts
:
tasks.register<Exec>("addFilesToXcodeproj") {
workingDir(layout.projectDirectory)
commandLine("bash", "-c", "./exportToXcode.sh")
}
note: if you change the default names of shared module, iosApp folder, iosApp.xcodeproj file and iosApp target, you'll have to adjust the exportToXcode.sh
accordingly (in # DEFAULT VALUES
section).
Step 3 - Import and use UIViewControllerRepresentable files in iOSApp
iOSApp
Now that the UIViewControllerRepresentable
files are included and referenced in the xcodeproj
, they are ready to be used:
import SwiftUI
import SharedUI
struct SomeView: View {
@State private var state: ViewState = ViewState(status: "default")
var body: some View {
ComposeViewRepresentable(viewState: $state, callback: {})
}
}
Pretty simple right? 😊
For a working sample run iosApp by opening iosApp/iosApp.xcodeproj
in Xcode and run standard configuration or use KMM plugin for Android Studio and choose iosApp
in run configurations.
Outputs
> Task :shared:kspKotlinIosSimulatorArm64
note: [ksp] loaded provider(s): [com.github.guilhe.kmp.composeuiviewcontroller.ksp.ProcessorProvider]
note: [ksp] GradientScreenUIViewController created!
note: [ksp] GradientScreenRepresentable created!
> Task :addFilesToXcodeproj
> Copying files to iosApp/SharedRepresentables/
> Checking for new references to be added to xcodeproj
> GradientScreenUIViewControllerRepresentable.swift added!
> Done
It's an example of a happy path 🙌🏼
You can also find another working sample in Expressus App:
Stability
Operation | Status |
---|---|
Android Studio Run | 🟢 |
Xcode Run | 🟢 |
Xcode Preview | 🟢 |
Occasionally, if you experience iosApp/SharedRepresentables
files not being updated after a successful build, try to run the following command manually:
./gradlew addFilesToXcodeproj
This could be due to gradle caches not being properly invalidated upon file updates.
If necessary, disable swift
files automatically export to Xcode and instead include them manually, all while keeping the advantages of code generation. Simply comment the following line:
//...configureEach { finalizedBy(":addFilesToXcodeproj") }
You will find the generated files under {shared-module}/build/generated/ksp/
.
Warning: avoid deleting iosApp/SharedRepresentables
whithout first using Xcode to Remove references
.
LICENSE
Copyright (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.