Compose Multiplatform is interoperable with the UIKit framework. You can embed Compose Multiplatform within a UIKit application as well as embed native UIKit components within Compose Multiplatform. This page provides examples both for using Compose Multiplatform inside a UIKit application and for embedding UIKit components inside Compose Multiplatform UI.
Use Compose Multiplatform inside a UIKit application
To use Compose Multiplatform inside a UIKit application, add your Compose Multiplatform code to any container view controller. This example uses Compose Multiplatform inside the UITabBarController class:
let composeViewController = Main_iosKt.ComposeOnly()
composeViewController.title = "Compose Multiplatform inside UIKit"
let anotherViewController = UIKitViewController()
anotherViewController.title = "UIKit"
// Set up the UITabBarController
let tabBarController = UITabBarController()
tabBarController.viewControllers = [
// Wrap the created ViewControllers in a UINavigationController to set titles
UINavigationController(rootViewController: composeViewController),
UINavigationController(rootViewController: anotherViewController)
]
tabBarController.tabBar.items?[0].title = "Compose"
tabBarController.tabBar.items?[1].title = "UIKit"
With this code, your application should look like this:
To use UIKit elements inside Compose Multiplatform, add the UIKit elements that you want to use to a UIKitView from Compose Multiplatform. You can write this code purely in Kotlin or use Swift as well.
Map view
You can implement a map view in Compose Multiplatform using the UIKit's MKMapView component. Set the component size by using the Modifier.size() or Modifier.fillMaxSize() functions from Compose Multiplatform:
With this code, your application should look like this:
Now, let's look at an advanced example. This code wraps UIKit's UITextField in Compose Multiplatform:
@OptIn(ExperimentalForeignApi::class)
@Composable
fun UseUITextField(modifier: Modifier = Modifier) {
// Holds the state of the text in Compose
var message by remember { mutableStateOf("Hello, World!") }
UIKitView(
factory = {
// Creates a UITextField integrated with Compose state
val textField = object : UITextField(CGRectMake(0.0, 0.0, 0.0, 0.0)) {
@ObjCAction
fun editingChanged() {
// Updates the Compose state when text changes in UITextField
message = text ?: ""
}
}
// Adds a listener for text changes within the UITextField
textField.addTarget(
target = textField,
action = NSSelectorFromString(textField::editingChanged.name),
forControlEvents = UIControlEventEditingChanged
)
textField
},
modifier = modifier.fillMaxWidth().height(30.dp),
update = { textField ->
// Updates UITextField text from Compose state
textField.text = message
}
)
}
The factory parameter contains the editingChanged() function and the textField.addTarget() listener to detect any changes to UITextField.
The editingChanged() function is annotated with @ObjCAction so that it can interoperate with Objective-C code.
The action parameter of the addTarget() function passes the name of the editingChanged() function, triggering it in response to a UIControlEventEditingChanged event.
The update parameter of UIKitView() is called when the observable message state changes its value.
The function updates the text attribute of the UITextField so that the user sees the updated value.
Explore the code for this example in our sample project.
This allows your application to access the device's camera and display a live preview.
Here's an example of how to implement a basic camera view:
UIKitView(
factory = {
val session = AVCaptureSession().apply {
val device = AVCaptureDevice.defaultDeviceWithMediaType(AVMediaTypeVideo)!!
val input = AVCaptureDeviceInput.deviceInputWithDevice(device, null)!!
addInput(input)
}
val previewLayer = AVCaptureVideoPreviewLayer(session)
session.startRunning()
object : UIView() {
override fun layoutSubviews() {
super.layoutSubviews()
previewLayer.frame = bounds
}
}.apply {
layer.addSublayer(previewLayer)
}
},
modifier = Modifier.size(300.dp)
)
Now, let's look at an advanced example. This code captures a photo, attaches GPS metadata, and displays a live preview using a native UIView:
@OptIn(ExperimentalForeignApi::class)
@Composable
fun RealDeviceCamera(
camera: AVCaptureDevice,
onCapture: (picture: PictureData.Camera, image: PlatformStorableImage) -> Unit
) {
// Initializes AVCapturePhotoOutput for photo capturing
val capturePhotoOutput = remember { AVCapturePhotoOutput() }
// ...
// Defines a delegate to capture callback: process image data, attach GPS, setup onCapture
val photoCaptureDelegate = remember {
object : NSObject(), AVCapturePhotoCaptureDelegateProtocol {
override fun captureOutput(
output: AVCapturePhotoOutput,
didFinishProcessingPhoto: AVCapturePhoto,
error: NSError?
) {
val photoData = didFinishProcessingPhoto.fileDataRepresentation()
if (photoData != null) {
val gps = locationManager.location?.toGps() ?: GpsPosition(0.0, 0.0)
val uiImage = UIImage(photoData)
onCapture(
createCameraPictureData(
name = nameAndDescription.name,
description = nameAndDescription.description,
gps = gps
),
IosStorableImage(uiImage)
)
}
capturePhotoStarted = false
}
}
}
// ...
// Sets up AVCaptureSession for photo capture
val captureSession: AVCaptureSession = remember {
AVCaptureSession().also { captureSession ->
captureSession.sessionPreset = AVCaptureSessionPresetPhoto
val captureDeviceInput: AVCaptureDeviceInput =
deviceInputWithDevice(device = camera, error = null)!!
captureSession.addInput(captureDeviceInput)
captureSession.addOutput(capturePhotoOutput)
}
}
// Sets up AVCaptureVideoPreviewLayer for the live camera preview
val cameraPreviewLayer = remember {
AVCaptureVideoPreviewLayer(session = captureSession)
}
// ...
// Creates a native UIView with the native camera preview layer
UIKitView(
modifier = Modifier.fillMaxSize().background(Color.Black),
factory = {
val cameraContainer = object: UIView(frame = CGRectZero.readValue()) {
override fun layoutSubviews() {
CATransaction.begin()
CATransaction.setValue(true, kCATransactionDisableActions)
layer.setFrame(frame)
cameraPreviewLayer.setFrame(frame)
CATransaction.commit()
}
}
cameraContainer.layer.addSublayer(cameraPreviewLayer)
cameraPreviewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill
captureSession.startRunning()
cameraContainer
},
)
// ...
// Creates a Compose button that executes the capturePhotoWithSettings callback when pressed
CircularButton(
imageVector = IconPhotoCamera,
modifier = Modifier.align(Alignment.BottomCenter).padding(36.dp),
enabled = !capturePhotoStarted,
) {
capturePhotoStarted = true
val photoSettings = AVCapturePhotoSettings.photoSettingsWithFormat(
format = mapOf(AVVideoCodecKey to AVVideoCodecTypeJPEG)
)
if (camera.position == AVCaptureDevicePositionFront) {
capturePhotoOutput.connectionWithMediaType(AVMediaTypeVideo)
?.automaticallyAdjustsVideoMirroring = false
capturePhotoOutput.connectionWithMediaType(AVMediaTypeVideo)
?.videoMirrored = true
}
capturePhotoOutput.capturePhotoWithSettings(
settings = photoSettings,
delegate = photoCaptureDelegate
)
}
}
The RealDeviceCamera composable performs the following tasks:
Sets up a native camera preview using AVCaptureSession and AVCaptureVideoPreviewLayer.
Creates a UIKitView that hosts a custom UIView subclass, which manages layout updates and embeds the preview layer.
Initializes a AVCapturePhotoOutput and configures a delegate to handle photo capture.
Uses CLLocationManager (through locationManager) to retrieve GPS coordinates at the moment of capture.
Converts the captured image into a UIImage, wraps it as a PlatformStorableImage, and provides metadata such as name, description, and GPS location via onCapture.
Displays a circular composable button for triggering the capture.
Applies mirroring settings when using the front-facing camera to match natural selfie behavior.
Updates the preview layout dynamically in layoutSubviews() using CATransaction to avoid animations.
You can implement a web view in Compose Multiplatform using UIKit's WKWebView component. This allows your application to display and interact with web content within the UI. Set the component size by using the Modifier.size() or Modifier.fillMaxSize() functions from Compose Multiplatform:
Now, let's look at an advanced example. This code configures the web view with navigation delegates and allows communication between Kotlin and JavaScript:
@Composable
fun WebViewWithDelegate(
modifier: Modifier = Modifier,
initialUrl: String = "http://www.jetbrains.com",
onNavigationChange: (String) -> Unit = {}
) {
// Creates a delegate to listen for navigation events
val delegate = remember {
object : NSObject(), WKNavigationDelegateProtocol {
override fun webView(
webView: WKWebView,
didFinishNavigation: WKNavigation?
) {
// Updates the current URL after navigation is complete
onNavigationChange(webView.URL?.absoluteString ?: "")
}
}
}
UIKitView(
modifier = modifier,
factory = {
// Instantiates a WKWebView and sets its delegate
val webView = WKWebView().apply {
navigationDelegate = delegate
loadRequest(NSURLRequest(uRL = NSURL(string = initialUrl)))
}
webView
},
update = { webView ->
// Reloads the web page if the URL changes
if (webView.URL?.absoluteString != initialUrl) {
webView.loadRequest(NSURLRequest(uRL = NSURL(string = initialUrl)))
}
}
)
}
The WebViewWithDelegate composable performs the following tasks:
Creates a stable delegate object implementing the WKNavigationDelegateProtocol interface. This object is remembered across recompositions using Compose's remember.
Instantiates a WKWebView, embeds it using UIKitView, and configures it assigning the remembered delegate.
Loads an initial web page provided by the initialUrl parameter.
Observes navigation changes via the delegate and passes the current URL through the onNavigationChange callback.
Uses the update parameter to observe changes in the requested URL and reloads the web page accordingly.