Compose Swift Bridge - Native iOS Views for Compose Multiplatform
Compose Swift Bridge is a tool for Compose Multiplatform mobile application developers. It generates Swift code to delegate Compose function calls to native iOS views. CSB supports both UIKit and SwiftUI view targets.
Compose Swift Bridge is Experimental
We built this library to help with boilerplate generation for our app development projects. The API is likely to change. Please submit issues in the repo. For PRs and feature requests, please reach out for a conversation first.
Compose Swift Bridge is an experimental tool that helps you define expect Composable functions to be
implemented later on the iOS Project using Swift. The tool works by generating Kotlin and Swift code
for expected Composable declarations annotated with @ExpectSwiftView
, as well as a View factory
interface to be implemented by hand in the iOS Project.
Also, the tool generates an ObservableObject for each parameter in the Expect Composable function
which easily keeps the state parameter updated in the SwiftUI View without any hassle. This is all possible by using SKIE Sub Plugin API and KSP.
@ExpectSwiftView
@Composable
expect fun MapView(
modifier: Modifier = Modifier,
title: String,
coordinate: MapCoordinates,
)
Tutorial
Configuring the project for the first time can be challenging, for that we have prepared a full tutorial on how to use the tool.
See our video on using native iOS views in Compose. Much of the manual work explained in the video is automated with CSB.
Annotation configurations
annotation class ExpectSwiftView(
val factoryName: String = "NativeView",
val type: ViewType = ViewType.SwiftUI,
val keepStateCrossNavigation: Boolean = false
)
The code generation works by generating the actual function in the iOS target, this function
also uses a generated Factory interface that contains all factory functions to each of the annotated
Composable function with @ExpectSwiftView
.
factoryName
: Defines the name of the generated factory interfaces. By default the name of the factory
is called NativeView
, which will generate the following functions and interfaces:
{factoryName}**Factory**
- Used on iOS for the implementing the factory**Local**{factoryName}**Factory**
- The local composition that you have to provide to the expect function to work
This is mostly important when you want to fragment in multiple factories with their own responsibility or when you are using Multi module setup and don’t want to use the KSP property, you should use a custom Factory Name for all your Expect Swift Views.
type
: Defines what is the View type that your native view will be at iOS Project, there are the following options: SwiftUI, UIViewController, UIKit.
- SwiftUI: The factory function receives a ObservableObject for each parameter at Composable function and requires to return a
AnyView
. - UIViewController: The factory function receives all the initial values from the Composable function and requires to return a Tuple of UIViewController and
{ComposableName}Delegate
. - UIKit View: The factory function receives all the initial values from the Composable function and requires to return a Tuple of View and
{ComposableName}Delegate
.
keepStateCrossNavigation
: Used when your native view (for example your SwiftUI View) has its own state
and it should be kept when navigating back. For example if you are replacing
an entire screen with SwiftUI and there is scrolling, if this is set to false,
when you navigate away from the screen and back, the scroll state will be
lose and the component will be recreated. By setting it to true the generator
will wrap your View inside a ViewModel that will survive the composition
and when going back will reuse the same ViewController storage in the ViewModel.
Notice: This should be avoided with small components that can easily be recreated without cost, because the ViewModel will survive even if you remove the Component from the Composition in the same screen. In this case it would cause an untended memory leak until the Screen that has started the Native view is disposed/removed from the stack. Note: This uses Androidx ViewModel KMP under the hood, check with your Navigation library if it is supported on the iOS target.
Samples
Configuring the interoperability function
The default Interoperability function being used in Compose Swift Bridge can be limited on configurations you might want to
use (for example configuring a UIKitInteropProperties
). To solve this problem we provide an annotation for defining
a custom Interoperability function that you can control the configuration of.
@ExpectSwiftView
@ExpectSwiftViewCustomInteropComposable("fully.qualified.name.of.your.Composable")
@Composable
expect fun MapView(
modifier: Modifier = Modifier,
title: String,
coordinate: MapCoordinates,
)
The Custom interop composable must have two arguments, and these arguments will depend on the type of your Expect View.
If it is a UIViewController and SwiftUI, the factory function parameter must return a UIViewController
. If it is a UIKit View,
it must return UIView
, also must have the same parameters names modifier
and factory
for both cases. Here is an example:
// SwiftUI or UIViewController example
@Composable
fun ExpectSwiftViewControllerCustom(
modifier: Modifier,
factory: () -> UIViewController
) {
UIKitViewController(
modifier = modifier,
factory = factory,
properties = UIKitInteropProperties(
isInteractive = false,
isNativeAccessibilityEnabled = true
)
)
}
// UIKit View example
@Composable
fun ExpectSwiftUIKitViewCustom(
modifier: Modifier,
factory: () -> UIView
) {
UIKitView(
factory = factory,
modifier = modifier,
properties = UIKitInteropProperties(
isInteractive = true,
isNativeAccessibilityEnabled = true
)
)
}
You can also configure the default interoperability function by using the following KSP Properties:
compose-swift-bridge.defaultViewControllerInteropComposableFqn
: Defines the interoperability function for SwiftUI and UIViewControllers.compose-swift-bridge.defaultUiKitViewInteropComposableFqn
: Defines the interoperability function for UIKit Views.
How the code generator works
The generator looks for all the functions annotated with @ExpectSwiftView
and their configurations. After it gets a list of those function it starts generating code using KSP.
- It will group all expect Composable functions in a group by
Factory Name
and Generate:- The
NativeViewFactory
, which is a Kotlin interface with factory functions for each expect Composable. It receives each initial parameter from it and returns aPair<UIViewController, {ComposableName}Delegate>
that will be used by the generated actual Composable. - The
Local Composition
for theNativeViewFactory
that the user of the tool will later need to update.
- The
- For each expect Composable it will generate:
- A
{ComposableName}Delegate
that is an interface that contains functions to handle state changes for each parameter of the Composable (for exampleupdateTitle(title: String)
). The generated ObservableObject uses this to keep the state in SwiftUI updated. The user can opt-in on the generation of the ObservableObject and implement the Delegate by themselves (for example integrating with UIKit). - If it is configured to use SwiftUI, it will generate an ObservableObject called
{ComposableName}Observable
that will contain Published states for each parameter of the expect Composable by implementing the{ComposableName}Delegate
interface. - The most important part is generating the actual function for the expect function you have annotated.
This is done by generating the actual function in the iOS Targets, and the internals containing the code to render UIViewController from the
the
NativeViewFactory
fetched from theLocal Composition
. It also keeps listening to states changes from the function parameter in order to call{ComposableName}Delegate
update functions. The generated code also handles if you configure withkeepStateCrossNavigation
. If so, it will store the UIViewController and the Delegate in a ViewModel (See the explanation ofkeepStateCrossNavigation
above).
- A