Kotlin Multiplatform – Bridging Compose & iOS UI Frameworks

Kotlin Multiplatform lets you share core business logic while still taking full advantage of native UI components. We explore practical approaches to integrating Kotlin Compose with iOS frameworks like UIKit and SwiftUI.

Mobile developers often face the challenge of balancing consistent business logic with the unique demands of each platform’s user interface. Kotlin Multiplatform (KMP) is a great choice for tackling this, allowing developers to write shared business logic while retaining platform-specific flexibility. 

When it comes to integrating UI frameworks like Compose with native iOS components (e.g., SwiftUI or UIKit), KMP offers multiple approaches tailored to various use cases. This article discusses the core expect/actual pattern – the foundation of KMP’s platform-specific customizations – and presents two additional strategies: using UIViewControllerRepresentable to integrate SwiftUI and directly accessing iOS native APIs through platform.UIKit.

The foundation: the expect/actual pattern in KMP

The expect/actual pattern is the cornerstone of Kotlin Multiplatform development. It enables developers to define shared abstractions (expect) and provide platform-specific implementations (actual). This approach is particularly useful when you want to share as much logic as possible while accommodating platform-specific details, including UI components.

Example:

Shared code: expect declaration

	
	
expect fun showPlatformDialog(message: String)

iOS implementation

	
	
actual fun showPlatformDialog(message: String) {
    val alert = UIAlertController(
        title = "iOS Dialog",
        message = message,
        preferredStyle = UIAlertControllerStyleAlert
    )
    alert.addAction(
        UIAlertAction.actionWithTitle("OK", UIAlertActionStyleDefault, null)
    )

    val rootViewController = UIApplication.sharedApplication.keyWindow?.rootViewController
    rootViewController?.presentViewController(alert, animated = true, completion = null)
}

Android implementation

	
	
actual fun showPlatformDialog(message: String) {
    AlertDialog.Builder(context)
        .setMessage(message)
        .setPositiveButton("OK", null)
        .show()
}

This pattern ensures:

  • Platform independence: Core logic remains in the shared module.
  • Flexibility: Platform-specific UI implementations can vary greatly.
  • Extensibility: New platforms can be added with minimal disruption.

While powerful, this pattern primarily handles business logic or modular UI components. More specialized use cases, such as embedding SwiftUI views or accessing native APIs directly, will require additional techniques.

1. Direct access to iOS APIs via platform.UIKit

For developers requiring granular control over native iOS functionalities, directly accessing APIs via platform.UIKit is a powerful alternative. This method uses UIKit directly, allowing you to manipulate UIKit components and system APIs directly in Kotlin.

Example:

Again, we’ll start with the first dialog example.

Shared code: expect declaration

	
	
expect fun showPlatformDialog(message: String)

iOS implementation: creating and displaying a native alert dialog

	
	
import platform.UIKit.*

actual fun showPlatformDialog() {
    val alert = UIAlertController(
        title = "Native Dialog",
        message = "This is a native dialog",
        preferredStyle = UIAlertControllerStyleAlert
    )
    alert.addAction(
        UIAlertAction.actionWithTitle("OK",
UIAlertActionStyleDefault, null)
    )

    val rootViewController =
UIApplication.sharedApplication.keyWindow?.rootViewController
    rootViewController?.presentViewController(alert, animated = true, completion = null)
}

Integrating in a Composable:

	
	
@Composable
fun MyButtonWithNativeDialog() {
    Button(onClick = {
        showPlatformDialog()
    }) {
        Text("Show Dialog")
    }
}

Pros:

  • Provides unrestricted access to native iOS capabilities.
  • Ideal for leveraging platform-specific APIs not available in SwiftUI or Compose.

Cons:

  • Provides unrestricted access to native iOS capabilities.
  • Writing and maintaining platform-specific code can increase complexity. UI consistency between platforms requires additional effort.

2. Integration with UIViewControllerRepresentable for SwiftUI on iOS

When dealing with existing SwiftUI components, the UIViewControllerRepresentable approach is invaluable. It wraps a SwiftUI view in a UIViewController that Kotlin Compose can embed. This is ideal for scenarios where SwiftUI views need to coexist with Compose.

Example:

SwiftUI Code

	
	
struct MySwiftUIView: View {
    var body: some View {
        VStack {
            Text("Hello from SwiftUI!")
            Button("Click Me") {
                print("Button clicked")
            }
        }
    }
}

Creating a UIViewControllerRepresentable

	
	
import SwiftUI
import UIKit

struct MySwiftUIViewController: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> UIHostingController<MySwiftUIView> {
        return UIHostingController(rootView: MySwiftUIView())
    }

    func updateUIViewController(_ uiViewController: UIHostingController<MySwiftUIView>, context: Context) {
        // Update logic, if needed
    }
}

Embedding the SwiftUI View Controller in Kotlin Compose

	
	
@Composable
fun MyComposableView() {
    UIKitView(
        factory = { MySwiftUIViewController() },
        modifier = Modifier.fillMaxSize()
    )
}

Pros:

  • Provides unrestricted access to native iOS capabilities.
  • Reuses SwiftUI components effortlessly within Kotlin Compose. Provides full access to SwiftUI’s advanced features.

Cons:

  • Overhead in managing state and synchronization between Compose and SwiftUI.
  • Bridging between two UI frameworks may affect performance for complex UIs.

Navigating Kotlin Compose and iOS frameworks

The interoperability between Kotlin Compose and iOS frameworks offers a wide range of possibilities, each with its strengths:

  • The expect/actual pattern serves as the cornerstone, enabling clean abstractions and cross-platform consistency.
  • Direct access to iOS APIs offers unmatched control for custom functionality, making it suitable for scenarios requiring precise native behavior.
  • SwiftUI integration provides a seamless way to reuse or embed native declarative components, ideal for projects prioritizing iOS-native aesthetics.

Choosing the right approach depends on your project’s goals and constraints. Thanks to Kotlin Multiplatform adaptability, developers are not tied to a single solution but can leverage the best of both Compose and native frameworks to build robust, flexible, and maintainable applications.