Kotlin Multiplatform Help

Integration with the SwiftUI framework

Compose Multiplatform is interoperable with the SwiftUI framework. You can embed Compose Multiplatform within a SwiftUI application as well as embed native SwiftUI components within the Compose Multiplatform UI. This page provides examples both for using Compose Multiplatform inside SwiftUI and for embedding SwiftUI inside a Compose Multiplatform app.

Use Compose Multiplatform inside a SwiftUI application

To use Compose Multiplatform inside a SwiftUI application, create a Kotlin function MainViewController() that returns UIViewController from UIKit and contains Compose Multiplatform code:

fun MainViewController(): UIViewController = ComposeUIViewController { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text("This is Compose code", fontSize = 20.sp) } }

ComposeUIViewController() is a Compose Multiplatform library function that accepts a composable function as the content argument. The function passed in this way can call other composable functions, for example, Text().

Next, you need a structure that represents Compose Multiplatform in SwiftUI. Create the following structure that converts a UIViewController instance to a SwiftUI view:

struct ComposeViewController: UIViewControllerRepresentable { func makeUIViewController(context: Context) -> UIViewController { return Main_iosKt.MainViewController() } func updateUIViewController(_ uiViewController: UIViewController, context: Context) { } }

Now you can use the ComposeViewController structure in other SwiftUI code.

Main_iosKt.MainViewController is a generated name. You can learn more about accessing Kotlin code from Swift on the Interoperability with Swift/Objective-C page.

In the end, your application should look like this:

ComposeView

For an example, see our sample project.

Size Compose views to fit their content

A Compose view can measure its content using a sizing proposal from SwiftUI and report its preferred size. If that size changes, the hosting UIView invalidates its intrinsic content size so SwiftUI can run layout again and apply the updated size.

For the UIViewRepresentable examples, define a Kotlin function MainComposeView() in main.ios.kt:

fun MainComposeView(): UIView = ComposeUIView { Text( "This Compose view wraps text to fit the available width", modifier = Modifier.fillMaxWidth().padding(16.dp) ) }

On iOS 16 and later

In your Swift code, pass SwiftUI's sizing proposal to the Compose view's sizeThatFits() method. Use UIView.noIntrinsicMetric for dimensions that SwiftUI leaves unspecified.

Use Compose content that wraps the dimension you want to fit, such as the Text in this section. If you adapt the example from Use Compose Multiplatform inside a SwiftUI application, replace its Box(Modifier.fillMaxSize()) with this content.

This UIViewRepresentable example uses a fixed width and content-based height. For a fixed height, use .frame(height: 100) and remove Modifier.fillMaxWidth().

struct ComposeView: UIViewRepresentable { let makeComposeUIView: () -> UIView func makeUIView(context: Context) -> UIView { makeComposeUIView() } func updateUIView(_ uiView: UIView, context: Context) {} @available(iOS 16.0, *) func sizeThatFits( _ proposal: ProposedViewSize, uiView: UIView, context: Context ) -> CGSize? { uiView.sizeThatFits( CGSize( // noIntrinsicMetric marks a dimension as unspecified by SwiftUI width: proposal.width ?? UIView.noIntrinsicMetric, height: proposal.height ?? UIView.noIntrinsicMetric ) ) } } struct ContentView: View { var body: some View { // Uses the bridge name generated for your Kotlin file // for example, main.ios.kt -> Main_iosKt ComposeView(makeComposeUIView: { Main_iosKt.MainComposeView() }) .frame(width: 240) } }
Size content hosted in a view controller

The MainViewController() Kotlin function from Use Compose Multiplatform inside a SwiftUI application returns a UIViewController instead of a UIView. To size its content on iOS 16 and later, first update MainViewController() in main.ios.kt to use content that wraps its height:

fun MainViewController(): UIViewController = ComposeUIViewController { Text( "This Compose view wraps text to fit the available width", modifier = Modifier.fillMaxWidth().padding(16.dp) ) }

In your Swift code, update the ComposeViewController wrapper from the example that uses UIViewControllerRepresentable to embed the controller in SwiftUI. Add sizeThatFits(_:uiViewController:context:) to this wrapper and forward SwiftUI's sizing proposal to the controller's Compose view:

struct ComposeViewController: UIViewControllerRepresentable { func makeUIViewController(context: Context) -> UIViewController { // Uses the bridge name generated for your Kotlin file // for example, main.ios.kt -> Main_iosKt Main_iosKt.MainViewController() } func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} @available(iOS 16.0, *) func sizeThatFits( _ proposal: ProposedViewSize, uiViewController: UIViewController, context: Context ) -> CGSize? { uiViewController.view.sizeThatFits( CGSize( // noIntrinsicMetric marks a dimension as unspecified by SwiftUI width: proposal.width ?? UIView.noIntrinsicMetric, height: proposal.height ?? UIView.noIntrinsicMetric ) ) } } struct ContentView: View { var body: some View { ComposeViewController() .frame(width: 240) } }

The fixed width constrains the text, and SwiftUI uses the reported height to fit the content.

Before iOS 16

If your SwiftUI app has to run on an iOS version earlier than 16, report intrinsic content size of MainComposeView() in main.ios.kt:

fun MainComposeView(): UIView = ComposeUIView( configure = { preferredSizeReportingStrategy = PreferredSizeReportingStrategy.IntrinsicContentSize } ) { Text( "This Compose view wraps text to fit the available width", modifier = Modifier.fillMaxWidth().padding(16.dp) ) }

This example uses a fixed width and lets the Compose content determine the height. In your Swift code, pass the width to sizeThatFits() in updateUIView() and use the same width for the SwiftUI frame. SwiftUI reads the resulting size through intrinsicContentSize:

struct IntrinsicComposeView: UIViewRepresentable { let makeComposeUIView: () -> UIView let width: CGFloat func makeUIView(context: Context) -> UIView { makeComposeUIView() } func updateUIView(_ uiView: UIView, context: Context) { _ = uiView.sizeThatFits( // noIntrinsicMetric marks a dimension as unspecified by SwiftUI CGSize(width: width, height: UIView.noIntrinsicMetric) ) } } struct ContentView: View { private let width: CGFloat = 240 var body: some View { // Uses the bridge name generated for your Kotlin file // for example, main.ios.kt -> Main_iosKt IntrinsicComposeView( makeComposeUIView: { Main_iosKt.MainComposeView() }, width: width ) .frame(width: width) } }

For a fixed height, swap width and height in the sizing proposal and SwiftUI frame.

Use SwiftUI inside Compose Multiplatform

To use SwiftUI inside Compose Multiplatform, add your Swift code to an intermediate UIViewController. Currently, you can't write SwiftUI structures directly in Kotlin. Instead, you have to write them in Swift and pass them to a Kotlin function.

To start, add an argument to your entry point function to create a ComposeUIViewController component:

@OptIn(ExperimentalForeignApi::class) fun ComposeEntryPointWithUIViewController( createUIViewController: () -> UIViewController ): UIViewController = ComposeUIViewController { Column( Modifier .fillMaxSize() .windowInsetsPadding(WindowInsets.systemBars), horizontalAlignment = Alignment.CenterHorizontally ) { Text("How to use SwiftUI inside Compose Multiplatform") UIKitViewController( factory = createUIViewController, modifier = Modifier.size(300.dp).border(2.dp, Color.Blue), ) } }

In your Swift code, pass the createUIViewController to your entry point function. You can use a UIHostingController instance to wrap SwiftUI views:

Main_iosKt.ComposeEntryPointWithUIViewController(createUIViewController: { () -> UIViewController in let swiftUIView = VStack { Text("SwiftUI in Compose Multiplatform") } return UIHostingController(rootView: swiftUIView) })

In the end, your application should look like this:

UIView

Explore the code for this example in the sample project.

Map view

You can implement a map view in Compose Multiplatform using SwiftUI's Map component. This allows your application to display fully interactive SwiftUI maps.

For the same Kotlin entry point function, in Swift, pass the UIViewController that wraps the Map view using a UIHostingController:

import SwiftUI import MapKit Main_iosKt.ComposeEntryPointWithUIViewController(createUIViewController: { let region = Binding.constant( MKCoordinateRegion( center: CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194), span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05) ) ) let mapView = Map(coordinateRegion: region) return UIHostingController(rootView: mapView) })

Now, let's look at an advanced example. This code adds a custom annotation to the SwiftUI map and allows you to update the view state from Swift:

import SwiftUI import MapKit struct AnnotatedMapView: View { // Manages map region state @State private var region = MKCoordinateRegion( center: CLLocationCoordinate2D(latitude: 51.5074, longitude: -0.1278), span: MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1) ) // Displays a map with a custom annotation var body: some View { Map(coordinateRegion: $region, annotationItems: [Landmark.example]) { landmark in MapMarker(coordinate: landmark.coordinate, tint: .blue) } } } struct Landmark: Identifiable { let id = UUID() let name: String let coordinate: CLLocationCoordinate2D static let example = Landmark( name: "Big Ben", coordinate: CLLocationCoordinate2D(latitude: 51.5007, longitude: -0.1246) ) }

You can then wrap this annotated map in a UIHostingController and pass it to your Compose Multiplatform code:

Main_iosKt.ComposeEntryPointWithUIViewController(createUIViewController: { return UIHostingController(rootView: AnnotatedMapView()) })

The AnnotatedMapView performs the following tasks:

  • Defines a SwiftUI Map view and embeds it inside a custom view called AnnotatedMapView.

  • Manages internal state for map positioning using @State and MKCoordinateRegion, allowing Compose Multiplatform to display an interactive, state-aware map.

  • Displays a MapMarker on the map using a static Landmark model that conforms to Identifiable, which is required for annotations in SwiftUI.

  • Uses annotationItems to declaratively place custom markers on the map.

  • Wraps the SwiftUI component inside a UIHostingController, which is then passed to Compose Multiplatform as a UIViewController.

Camera view

You can implement a camera view in Compose Multiplatform using SwiftUI and UIKit's UIImagePickerController, wrapped in a SwiftUI-compatible component. This allows your application to launch the system camera and capture photos.

For the same Kotlin entry point function, in Swift, define a basic CameraView using UIImagePickerController and embed it using UIHostingController:

Main_iosKt.ComposeEntryPointWithUIViewController(createUIViewController: { return UIHostingController(rootView: CameraView { image in // Handle captured image here }) })

To make this work, define CameraView as follows:

import SwiftUI import UIKit struct CameraView: UIViewControllerRepresentable { let imageHandler: (UIImage) -> Void @Environment(\.presentationMode) private var presentationMode init(imageHandler: @escaping (UIImage) -> Void) { self.imageHandler = imageHandler } func makeCoordinator() -> Coordinator { Coordinator(self) } func makeUIViewController(context: Context) -> UIImagePickerController { let picker = UIImagePickerController() picker.sourceType = .camera picker.delegate = context.coordinator return picker } func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {} class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate { let parent: CameraView init(_ parent: CameraView) { self.parent = parent } func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { if let image = info[.originalImage] as? UIImage { parent.imageHandler(image) } parent.presentationMode.wrappedValue.dismiss() } func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { parent.presentationMode.wrappedValue.dismiss() } } }

Now, let's look at an advanced example. This code presents a camera view and displays a thumbnail of the captured image in the same SwiftUI view:

import SwiftUI import UIKit struct CameraPreview: View { // Controls the camera sheet visibility @State private var showCamera = false // Stores the captured image @State private var capturedImage: UIImage? var body: some View { VStack { if let image = capturedImage { // Displays the captured image Image(uiImage: image) .resizable() .scaledToFit() .frame(height: 200) } else { // Shows placeholder text when no image is captured Text("No image captured") } // Adds a button to open the camera Button("Open Camera") { showCamera = true } // Presents CameraView as a modal sheet .sheet(isPresented: $showCamera) { CameraView { image in capturedImage = image } } } } }

The CameraPreview view performs the following tasks:

  • Presents a CameraView in a modal .sheet when the user taps a button.

  • Uses the @State property wrapper to store and display the captured image.

  • Embeds SwiftUI's native Image view to preview the photo.

  • Reuses the same UIViewControllerRepresentable-based CameraView as before, but integrates it more deeply into the SwiftUI state system.

Web view

You can implement a web view in Compose Multiplatform using SwiftUI by wrapping UIKit's WKWebView component with UIViewRepresentable. This allows you to display embedded web content with full native rendering.

For the same Kotlin entry point function, in Swift, define a basic WebView embedded using UIHostingController:

Main_iosKt.ComposeEntryPointWithUIViewController(createUIViewController: { let url = URL(string: "https://www.jetbrains.com")! return UIHostingController(rootView: WebView(url: url)) })

Now, let's look at an advanced example. This code adds navigation tracking and loading state display to the web view:

import SwiftUI import UIKit import WebKit struct AdvancedWebView: UIViewRepresentable { let url: URL @Binding var isLoading: Bool @Binding var currentURL: String // Creates WKWebView with navigation delegate func makeUIView(context: Context) -> WKWebView { let webView = WKWebView() webView.navigationDelegate = context.coordinator webView.load(URLRequest(url: url)) return webView } func updateUIView(_ uiView: WKWebView, context: Context) {} // Creates coordinator to handle web navigation events func makeCoordinator() -> Coordinator { Coordinator(isLoading: $isLoading, currentURL: $currentURL) } class Coordinator: NSObject, WKNavigationDelegate { @Binding var isLoading: Bool @Binding var currentURL: String init(isLoading: Binding<Bool>, currentURL: Binding<String>) { _isLoading = isLoading _currentURL = currentURL } func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation?) { isLoading = true } // Updates URL and indicates loading has completed func webView(_ webView: WKWebView, didFinish navigation: WKNavigation?) { isLoading = false currentURL = webView.url?.absoluteString ?? "" } } }

Use it in a SwiftUI view as follows:

struct WebViewContainer: View { // Tracks loading state of web view @State private var isLoading = false // Tracks current URL displayed @State private var currentURL = "" var body: some View { VStack { // Displays loading indicator while loading if isLoading { ProgressView() } // Shows current URL Text("URL: \(currentURL)") .font(.caption) .lineLimit(1) .truncationMode(.middle) // Embeds the advanced web view AdvancedWebView( url: URL(string: "https://www.jetbrains.com")!, isLoading: $isLoading, currentURL: $currentURL ) } } }

The AdvancedWebView and WebViewContainer perform the following tasks:

  • Create a WKWebView with a custom navigation delegate to track loading progress and URL changes.

  • Use SwiftUI's @State bindings to dynamically update the UI in response to navigation events.

  • Display a ProgressView spinner while a page is loading.

  • Show the current URL at the top of the view using a Text component.

  • Use UIHostingController to integrate this component into your Compose UI.

What's next

10 November 2026