KINTO Tech Blog
Development

SwiftUI in Compose Multiplatform of KMP

Rasel
Rasel
Cover Image for SwiftUI in Compose Multiplatform of KMP

Introduction

Hello, this is Rasel from the Mobile Application Development group of KINTO Technologies. Currently I’m working in my route Android application. my route is an Outing Multimodal application that will assist you in gathering information about places to visit, exploring various locations on the map, buying digital tickets, making reservations, handling payments for rides etc.

As you already know, mobile applications became an essential part of our daily lives. Developers primarily create applications targeting Android and iOS platforms separately, incurring in double the cost for both platforms. To reduce those development costs, various Cross-Platform application development frameworks like React Native, Flutter etc. have emerged.

But there is always complains about the performance of these cross-platform apps. They don’t offer performance like natively developed apps. Also, there is always issues and sometimes we have to wait longer to get support of new features from framework developers whenever platform-specific new features are released by Android and iOS.

Here comes Kotlin Multiplatform (KMP) to the rescue, which offers native-like performance along with the freedom to choose how much code to share between platforms. In KMP, the Android application is fully native as it is being developed with Kotlin, Android's native first language, so there is almost no performance issues. The iOS part uses Kotlin/Native which offers a performance that is closer to natively developed apps when compared to any other frameworks.

Today, in this article, we are going to show you how to integrate SwiftUI code along with Compose Multiplatform in KMP.

KMP (Also known as KMM for mobile platforms) gives you the freedom to choose how much code you want to share between platforms, and how much code you want to implement natively. It integrates seamlessly with platform codes. Previously it was possible to only share business logic between platforms, but now you can also share UI codes too! Sharing UI codes became possible with Compose Multiplatform. You can read our previous article on this topic below to better understand the usage of Kotlin Multiplatform and Compose Multiplatform in mobile application development.

So, let’s get started~

Overview

To demonstrate the SwiftUI integration into Compose Multiplatform, we will use a very simple Gemini Chat application. We will develop the app with KMP which will use Compose Multiplatform for UI development. And will use Google’s Gemini Pro API for replying to user’s query in chat. For demonstration purposes, also to keep it simple, we are going to use the free version of API so only text messages are allowed.

How Compose and SwiftUI works together

First things first. Let's create a KMP project using Jetbrain's Kotlin Multiplatform Wizard which comes with necessary basic setup of KMP with Compose Multiplatform and some initial SwiftUI code.
Kotlin Multiplatform Wizard

You can also create the project using Android Studio IDE by installing Kotlin Multiplatform Mobile plugin into it.

We will try to demonstrate how Compose and SwiftUI works together. To incorporate our Composable code into iOS, we have to wrap our Composable code inside ComposeUIViewController which returns UIViewController from UIKit and can contain compose code inside it as content parameter. For example:

// MainViewController.kt
fun ComposeEntryPoint(): UIViewController {
    return ComposeUIViewController {
        Column(
            modifier = Modifier.fillMaxSize(),
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.Center
        ) {
            Text(text = "Hello from Compose")
        }
    }
}

Then we will call this function from iOS side. For that, we need a structure which represents Compose code in SwiftUI. Below code will convert our UIViewController code of shared module into a SwiftUI view:

// ComposeViewControllerRepresentable.swift
struct ComposeViewControllerRepresentable : UIViewControllerRepresentable {
    
    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {}
    
    func makeUIViewController(context: Context) -> some UIViewController {
        return MainViewControllerKt.ComposeEntryPoint()
    }
}

Here, take a closer look to the name of MainViewControllerKt.ComposeEntryPoint(). This will be our generated code from Kotlin. So, it might be different according to your file name and code inside shared module. Suppose, if your file name in shared module is Main.ios.kt and your UIViewController returning function name is ComposeEntryPoint(), then you have to call it like Main_iosKt.ComposeEntryPoint(). So it will differ according to your code.

Now we will instantiate this ComposeViewControllerRepresentable from inside of our ContentView() code and we are good to go.

// ContentView.swift
struct ContentView: View {
    var body: some View {
        ComposeViewControllerRepresentable()
            .ignoresSafeArea(.all)
    }
}

As you can see in the code, you can use this Compose code anywhere inside SwiftUI and control it’s size as you want from within SwiftUI.
The UI will look like as:
Hello from Swift

If you want to integrate SwiftUI code inside compose, you have to wrap it with UIView, as you can't write SwiftUI code directly in Kotlin, you have to write it in Swift and pass it to a Kotlin function. To implement it, let's add an argument of type UIView to our ComposeEntryPoint() function.

// MainViewController.kt
fun ComposeEntryPoint(createUIView: () -> UIView): UIViewController {
    return ComposeUIViewController {
        Column(
            modifier = Modifier.fillMaxSize(),
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.Center
        ) {
            UIKitView(
                factory = createUIView,
                modifier = Modifier.fillMaxWidth().height(500.dp),
            )
        }
    }
}

And pass createUIView to our Swift code as below:

// ComposeViewControllerRepresentable.swift
struct ComposeViewControllerRepresentable : UIViewControllerRepresentable {
    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {}
    
    func makeUIViewController(context: Context) -> some UIViewController {
        return MainViewControllerKt.ComposeEntryPoint(createUIView: { () -> UIView in
            UIView()
        })
    }
}

Now, if you want to add other Views, create an parent wrapper UIView like below:

// ComposeViewControllerRepresentable.swift
private class SwiftUIInUIView<Content: View>: UIView {
    init(content: Content) {
        super.init(frame: CGRect())
        let hostingController = UIHostingController(rootView: content)
        hostingController.view.translatesAutoresizingMaskIntoConstraints = false
        addSubview(hostingController.view)
        NSLayoutConstraint.activate([
            hostingController.view.topAnchor.constraint(equalTo: topAnchor),
            hostingController.view.leadingAnchor.constraint(equalTo: leadingAnchor),
            hostingController.view.trailingAnchor.constraint(equalTo: trailingAnchor),
            hostingController.view.bottomAnchor.constraint(equalTo: bottomAnchor)
        ])
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

Then add it into your ComposeViewControllerRepresentable and add Views according to your needs:

// ComposeViewControllerRepresentable.swift
func makeUIViewController(context: Context) -> some UIViewController {
    return MainViewControllerKt.ComposeEntryPoint(createUIView: { () -> UIView in
            SwiftUIInUIView(content: VStack {
                Text("Hello from SwiftUI")
                Image(systemName: "moon.stars")
                    .resizable()
                    .frame(width: 200, height: 200)
            })
    })
}

The output will look like this:
Hello from Swift with Image
In this way, you can add as much SwiftUI code as you want into your shared Composable codes.

And if you want to integrate UIKit code inside Compose, you don't have to write any intermediate code yourself. You can use UIKitView() composable function offered by Compose Multiplatform and add your UIKit code inside it directly:

// MainViewController.kt
UIKitView(
  modifier = Modifier.fillMaxWidth().height(350.dp),
  factory = { MKMapView() }
)

This code integrates iOS native Map screen inside compose.

Implementation of Gemini Chat app

Now, let’s integrate our Compose code inside SwiftUI and proceed with the implementation of Gemini Chat app. We will implement a basic chat UI using LazyColumn of Jetpack Compose. As our main focus is integrating SwiftUI inside Compose Multiplatform, we are ignoring implementation details of other parts of the application like Compose part or data and logic part.
We are using Ktor networking library to implement Gemini Pro API. To know more about Ktor implementation, visit Creating a cross-platform mobile application page.

In this project, we are implementing our full UI with Compose Multiplatform. We will use SwiftUI just for input field of iOS app as TextField of Compose Multiplatform has some performance glitch in iOS side.

Let’s put our Compose code inside ComposeEntryPoint() function. These codes contains Chat UI with TopAppBar and list of messages. This also has conditional implementation of input field which will be used for Android app.

// MainViewController.kt
fun ComposeEntryPoint(): UIViewController =
    ComposeUIViewController {
        Column(
            Modifier
                .fillMaxSize()
                .windowInsetsPadding(WindowInsets.systemBars),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            ChatApp(displayTextField = false)
        }
    }

We passed false to displayTextField so that Compose input field will not be active for our iOS version of the app. And the value of displayTextField will be true when we call dis ChatApp() composable function from Android implementation side as there is no performance issue of TextField in Android side (It’s native UI component for Android).

Now come to our Swift code and implement a input field with SwiftUI:

// TextInputView.swift
struct TextInputView: View {
    @Binding var inputText: String
    @FocusState private var isFocused: Bool

    var body: some View {
        VStack {
            Spacer()
            HStack {
                TextField("Type message...", text: $inputText, axis: .vertical)
                    .focused($isFocused)
                    .lineLimit(3)
                if (!inputText.isEmpty) {
                    Button {
                        sendMessage(inputText)
                        isFocused = false
                        inputText = ""
                    } label: {
                        Image(systemName: "arrow.up.circle.fill")
                            .tint(Color(red: 0.671, green: 0.365, blue: 0.792))
                    }
                }
            }
            .padding(15)
            .background(RoundedRectangle(cornerRadius: 200).fill(.white).opacity(0.95))
            .padding(15)
        }
    }
}

And then return back to our ContentView structure and modify it like below:

// ContentView.swift
struct ContentView: View {
    @State private var inputText = ""
    
    var body: some View {
        ZStack {
            Color("TopGradient")
                .ignoresSafeArea()
            ComposeViewControllerRepresentable()
            TextInputView(inputText: $inputText)
        }
        .onTapGesture {
            // Hide keyboard on tap outside of TextField
            UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
        }
    }
}

Here, we added a ZStack and inside it we added our TopGradient color and also ignoresSafeArea() modifier so that our status bar color also matches rest of the our UI.

Then we added our shared Compose code wrapper ComposeViewControllerRepresentable which implemented our main Chat UI. Then we also added our SwiftUI view named TextInputView() which will give smooth performance to the user in iOS app too with iOS native code. The final UI will look like this:

Gemini Chat iOS Gemini Chat Android
Gemini Chat iOS Gemini Chat Android

Here, the whole UI code of this ChatApp is shared between Android and iOS with Compose Multiplatform of KMP and only input field for iOS is integrated natively with SwiftUI.

The complete source code for this project is available on GitHub as a public repository.

GitHub Repository: SwiftUI in Compose Multiplatform of KMP

Conclusion

In this way, we can overcome our performance issues of Cross-Platform app with Kotlin Multiplatform and Compose Multiplatform while giving native-like feels and look to the user. We can also reduce development cost as we can share codes between platforms as much as we want. Compose Multiplatform also enables to share code with Desktop applications too. So single codebase can be used in mobile platforms as well as Desktop apps. Additionally, web support is in progress which will give you more opportunities to share codebase between platforms. Another big advantage of Kotlin Multiplatform (KMP) is, you can always opt out to your native development without wasting your code. You can use KMP code AS-IS in your Android application as it’s native for Android, and opt-out to develop iOS app separately. Also, reusing the same SwiftUI codes you have already implemented in KMP is possible. This framework not only gives you high-performance applications, but also the freedom to choose between percentages of code to share and to opt-out into native development, anytime you want.

That's all for today. Stay tuned to updates on the KINTO Technologies Tech Blog for more exciting articles. Happy Coding!

Facebook

関連記事 | Related Posts

We are hiring!

【iOS/Androidエンジニア】モバイルアプリ開発G/東京・大阪

モバイルアプリ開発GについてKINTOテクノロジーズにおける、モバイルアプリ開発のスペシャリストが集まっているグループです。KINTOやmy routeなどのサービスを開発・運用しているグループと協調しながら品質の高いモバイルアプリを開発し、サービスの発展に貢献する事を目標としています。

【部長・部長候補】/プラットフォーム開発部/東京

プラットフォーム開発部 について共通サービス開発GWebサービスやモバイルアプリの開発において、必要となる共通機能=会員プラットフォームや決済プラットフォームの開発を手がけるグループです。KINTOの名前が付くサービスやTFS関連のサービスをひとつのアカウントで利用できるよう、様々な共通機能を構築することを目的としています。