Compose Swift Bridge

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.

Code diagram

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.

Tutorial

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.

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.

  1. It will group all expect Composable functions in a group by Factory Name and Generate:
    1. The NativeViewFactory, which is a Kotlin interface with factory functions for each expect Composable. It receives each initial parameter from it and returns a Pair<UIViewController, {ComposableName}Delegate> that will be used by the generated actual Composable.
    2. The Local Composition for the NativeViewFactory that the user of the tool will later need to update.
  2. For each expect Composable it will generate:
    1. A {ComposableName}Delegate that is an interface that contains functions to handle state changes for each parameter of the Composable (for example updateTitle(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).
    2. 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.
    3. 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 the Local 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 with keepStateCrossNavigation. If so, it will store the UIViewController and the Delegate in a ViewModel (See the explanation of keepStateCrossNavigation above).