· 7 min read Posted by Touchlab

Compose UI for iOS

While Compose for iOS is not production ready yet and may never fully replace native iOS UI, it’s definitely worth it to try it out.

Authors
Júlia Jakubcová & Tadeas Kriz

We tried (successfully) to use Compose for both iOS and Android UI of the Droidcon app (read more here). The app is written in Kotlin Multiplatform, and the original iOS views (which can still be used by changing a toggle switch in the Settings) are in SwiftUI. The view models are written in Kotlin. The Compose for the iOS is definitely not production ready (at the time of writing this) but we didn’t encounter any major obstacles and to be honest, it was a lot of fun.


DROIDCONKOTLIN REPO

https://github.com/touchlab/DroidconKotlin


Why

Why should anyone use Compose for iOS views? One answer would be to minimize the code that needs to be written. You can share the views or their parts with Android and save time and effort for programmers. It’s also written in Kotlin, so it minimizes the need to have a developer team specialized in Swift (even though some Swift code still needs to be written, but not much). Another point is better compatibility with view models written in Kotlin. You can directly observe changes in StateFlows without the need to write additional code (as you often need to in Multiplatform development when connecting view models to the SwiftUI views). Or if you for some reason want to use Material design for your iOS app. Or if your app is very much stylized for both platforms, with Compose, you need to create your own styled views only once and in one language. If you are still not convinced, a good reason to use Compose for iOS is to just try it out and have some fun, and maybe a use case will come along.

How

Jetpack Compose’s iOS entrypoint function Application(title: String, content: @Composable () -\> Unit) returns an instance of UIViewController. Since we’re using SwiftUI in Droidcon, we need to wrap it in an implementation of a UIViewControllerRepresentable. Unfortunately, the application was crashing when switching back from the background, or when switching from SwiftUI to Jetpack Compose from iOS settings. Those crashes were caused by the incorrect size of the view, so we wrapped the Compose controller in a container controller. This delays the need for view size until Compose can get the correct size from iOS.

We then switch between the SwiftUI implementation, or the Jetpack Compose one, depending on a setting passed down from a view model. If we didn’t use SwiftUI’s  App implementation (with the @main attribute), we could’ve just instantiated a new UIWindow with the Jetpack Compose view controller as its root. It would allow us to skip implementing the UIViewControllerRepresentable and possibly even the workaround mentioned above.

One of the limitations of Jetpack Compose compiled with Kotlin/Native is the requirement for all @Composable symbols to be internal or private. Any public composables will result in a compile error. For that reason, we hide the call to the Application entrypoint inside a global function getRootController and return the controller’s instance from it.

Drawing images

It’s possible to draw images from Xcode Assets by creating a BitmapPainter from compose ImageBitmap converted from Skia Image that was converted from UIKit’s UIImage. The conversion to Skia Image consists of remapping all the values and properties, such as width and height, but also bytes per row, alpha type and colour type. The Jetbrains’s Skia Image has limited support for color spaces and at the time of writing doesn’t support grayscale images. We haven’t spent the time to implement color space conversion in runtime and instead made sure all images in the app have one of the supported color spaces. Our implementation also doesn’t support color order other than RGBA_8888 as Skia’s and UIKit’s implementation are different.

Hyperdrive Singularity

The app currently uses a library called Hyperdrive Singularity for shared view models. To understand why we need to look into the history of the Droidcon app. In July 2021, a rewrite of the Droidcon app was made in Jetpack Compose on Android and SwiftUI on iOS. The rewrite was done by a company Tadeas founded long before joining Touchlab, and they used the Singularity library in the iOS app, to showcase it to Kevin.

When we began trying the Jetpack Compose for iOS, we were skeptical and started small. Initially, we didn’t think there would be any shared UI code between the iOS and Android Compose implementations. So the Composables on iOS were written from scratch and Singularity’s support for Jetpack Compose came in handy. Since Singularity is a multiplatform library and supports Android, we were able to make the Compose UI code shared and replace the previous implementation we had that was using Android ViewModels.

That said, there are other libraries that can be used to write shared view models (it can also be done with no libraries, manually). It was just convenient for us to keep view models already written with Singularity and save ourselves time.


CAUTION

Hyperdrive Singularity was added as an experiment and is not made by, nor endorsed by Touchlab. Use at your own risk.


Demo user’s perspective

After the initial set-up it’s fairly easy to use. You can create composable functions with your views as you normally would, use the same theming as on Android, use material icons and components like BottomNavigationBarTopAppBarScaffoldLazyColumnRow, … Even the ripple effect works, though items stay a little lighter until they lose focus (user clicks on something else) on both simulator and on a device. Sadly there is no effect when the end of a scrollable view is reached yet.

Another problem is navigation. For the Droidcon app we used Compose for all of the iOS screens, calling the main compose controller from the main SwiftUI View. Navigation is handled by a custom component: NavigationController holding and managing a back stack, NavigationStack and NavigationLinkNavigationStack has a composable function – parent view – and a list of links where the user can navigate to from the said screen. When a child view should be shown is indicated by a view model value for this screen turning from null to an instance (in parent view model). In NavigationStack views with non-null values are shown by being placed in a SubcomposeLayout. We added an animation by using AnimatedContent to have nice transitions to children views and back to the parent. It’s possible to use just one or even all the views in compose while the rest is in SwiftUI by having a UIViewController which can be called from SwiftUI code (for example in SwiftUI’s NavigationLink to open the compose view as a new screen). It’s important to make sure the size of the compose view is correct, in some instances (compose view inside a view with bottom navigation) we had to set the height manually from a GeometryReader because the view would cover the space behind the bottom navigation row as well.

Sharing compose views between Android and iOS

By having the compose views in a shared module, we can share them between both Android and iOS, so then each view is written only once and all the changes and fixes can be done in a single place. For cases when we want some view implemented differently for each platform we can use the expect/actual from KMP. For example having a DialogView composable expect function that takes composable content as it’s parameter and implementing it on Android with the native Dialog and on iOS with a custom Box with semi-transparent background. It’s also useful for images, since we need to get them from the assets/resources differently on each platform. This can also be used for example if we want to have different options for each platform in the settings screen. To be able to have the views common we need common view models and use the <NavigationController for the navigation on Android as well.

Conclusion

While Compose for iOS is not production ready yet and may never fully replace native iOS UI, it’s definitely worth it to try it out and in the future I’m sure it will find it’s place in codebases of many Multiplatform applications. If you’re trying this out for yourself and need some help, you can reach out to Touchlab.