When you use Compose Multiplatform on iOS, the Kotlin code for your UI is compiled to native code using Kotlin/Native. This native code is then used to create a UIKit-based UI that runs on the iOS platform. Compose Multiplatform for iOS provides the Kotlin APIs for building Compose UIs on iOS. This library bridges the gap between the Kotlin/Native code and the UIKit framework.

Kotlin code
|
v
Kotlin/Native compiler
|
v
Native code for iOS
|
v
UIKit framework
|
v
Compose UI

The alpha² release of Compose Multiplatform introduces a prototype for bidirectional interaction at the UI level. With the help of UIKitView, you can seamlessly integrate intricate platform-specific widgets — such as maps, web views, media players, and camera feeds — into your shared user interface. Conversely, employing ComposeUIViewController allows you to nest Compose Multiplatform screens within SwiftUI applications, facilitating a gradual integration of Compose Multiplatform into your iOS apps.

Our emphasis will be on the latter.

By this, I’m referring to a screen implementation in which state changes are managed by the multiplatform (shared-ui) module, in Kotlin. This implies that on the iOS side, our task involves just creating a container to display it.

Let me simplify this with an example. We’ll create a Composable containing two buttons for navigating between screens. On iOS, we’ll include this Composable and set up our navigation.

In the shared-ui module, we create a function that gives us a UIViewController. This function wraps our Composable implementation within ComposeUIViewController:

fun selector(onClickA: () -> Unit, onClickB () -> Unit): UIViewController {
return ComposeUIViewController { /*compose implementation...*/ }
}

In iOS we create a UIViewControllerRepresentable that will import the ComposeUIViewController:

private struct SelectorUIViewController: UIViewControllerRepresentable {

let showScreenA: () -> Void
let showScreenB: () -> Void

func makeUIViewController(context: Context) -> UIViewController {
return SharedViewControllersKt.selector(onClickA: showScreenA, onClickB: showScreenB)
}

func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}

We finish it by setting up our navigation:

private enum Destination: Hashable {
case screenA
case screenB
}

struct HomeScreen: View {

@State var navigation = NavigationPath()

var body: some View {
NavigationStack(path: $navigation) {
SelectorUIViewController(
showScreenA: { navigation.append(Destination.screenA) },
showScreenB: { navigation.append(Destination.screenB) }
)
.navigationDestination(for: Destination.self) { destination in
switch destination {
case .screenA:
ScreenA()
case .screenB:
ScreenB()
}
}
.ignoresSafeArea()
}
}
}

And it’s done. We have a Composable inside a SwiftUI View.

By this, I’m referring to a screen implementation in which state changes are not managed by the shared-ui module. This implies that on the iOS side, we must forward state changes to the shared-ui module.

In this example, we have a SwiftUI View where a ViewModel handles the state, passing changes to State and Binding properties:

struct StatusScreen: View {

@StateObject private var viewModel = StatusViewModel()
@State private var status: String = ""

var body: some View {
StatusUIViewController(
status: $status,
action: { viewModel.changeStatus() }
)
.onReceive(viewModel.$state) { new in
status = new.status
}
.ignoresSafeArea()
}
}

Our UIViewControllerRepresentable:

struct StatusUIViewController: UIViewControllerRepresentable {

@Binding var status: String
let action: () -> Void

func makeUIViewController(context: Context) -> UIViewController {
return SharedViewControllers().statusComposable(status: status, click: action)
}

func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
//how to update the composable state when binding values changes...?
}
}

Despite the invocation of updateUIViewController, the Multiplatform Compose API lacks³ a mechanism to communicate these changes to the Composable.

Thankfully, there’s a workaround we can employ. Within our ComposeUIViewController, we gather state changes using a MutableStateFlow. Subsequently, we expose a function for iOS to invoke, facilitating the emission of these changes:

object SharedViewControllers {

private data class ComposeUIViewState(val status: String = "")
private val state = MutableStateFlow(ComposeUIViewState())

fun statusComposable(click: () -> Unit): UIViewController {
return ComposeUIViewController {
with(state.collectAsState().value) {
StatusComposable(state.status, click)
}
}
}

fun updateStatusComposable(status: String) {
state.update { it.copy(status = status) }
}
}

And then our updateUIViewController becomes:

func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
SharedViewControllers().updateStatusComposable(status: status)
}

While not the most elegant approach, it’s essential to keep in mind that we’re working with an alpha version. With time, we can anticipate significant enhancements and refinements.

Source link