SwiftUIをCompose Multiplatformで使用する

はじめに
こんにちは。KINTOテクノロジーズモバイルアプリケーション開発グループのRaselです。私は現在、my route Androidアプリの開発に取り組んでいます。my routeは、外出時に利用するマルチモーダルアプリで、目的地の情報収集、地図上のさまざまな場所の探索、デジタルチケットの購入、予約、乗車料金の支払い処理などを行うことができます。
いまやモバイルアプリは私たちの日常生活に欠かせないものです。我々のようなエンジニアは、AndroidとiOSアプリをそれぞれ別で作成するため、両方のプラットフォームを開発するためにはダブルコストが発生します。これらの開発コストを削減するためにReact Native、Flutterなど、様々なクロスプラットフォームフレームワークが登場しました。
しかし、クロスプラットフォームアプリのパフォーマンスには常に課題があります。ネイティブアプリのようなパフォーマンスではありません。また、プラットフォーム固有の新機能がAndroidやiOSからリリースされると、フレームワーク開発者からサポートを受けなければいけない場合があり、さらに時間がかかります。
そこで Kotlin Multiplatform (KMP) が助けになります。ネイティブアプリ並みのパフォーマンスで、プラットフォーム間で共有するコードを自由に選択できるのです。KMPでは、Androidのネイティブ第一言語であるKotlinでAndroidアプリが開発されていて、完全にネイティブなので、パフォーマンス上の問題はほとんどありません。iOSの部分はKotlin/Native
を使用しており、他のフレームワークと比較して、ネイティブアプリとして開発されたものに近いパフォーマンスがあります。
本記事では、SwiftUIコードをCompose Multiplatformと統合する方法を紹介します。
KMP(モバイルプラットフォームではKMMとしても知られています)では、プラットフォーム間で共有するコードの量と、ネイティブアプリに実装するコードを自由に選択でき、プラットフォームのコードとシームレスに統合されます。以前は、ビジネスロジックのみをプラットフォーム間で共有できましたが、今では UI コードも共有できるようになりました。Compose Multiplatformにおいても、 UIコードの共有が可能になりました。下にある以前の記事を読むと、モバイルアプリ開発におけるKotlin MultiplatformとCompose Multiplatformの使用法をよりよく理解できます。
- Kotlin Multiplatform Mobile (KMM)を使ったモバイルアプリ開発
- Kotlin Multiplatform Mobile(KMM)およびCompose Multiplatformを使用したモバイルアプリケーションの開発
それでは、始めましょう!
概要
我々はUI開発でCompose Multiplatformを使用するKMPを用いてアプリ開発をしています。今回はSwiftUIをCompose Multiplatformに統合する方法を示すため、とてもシンプルなGeminiチャットアプリを使用します。また、チャットでのユーザーのクエリへの返信には、GoogleのGemini Pro APIを使用します。デモすることが目的なので、シンプルにするためにも、テキストメッセージのみが許可されるよう無料版の API を使用します。
Compose と SwiftUI がどのように連携するか
まず、最初に大事なことから。JetbrainのKotlin Multiplatform Wizard を使用してKMPプロジェクトを作成します。このウィザードには、必要になるKMPの基本的なセットアップと、Compose Multiplatformと、いくつかの初期SwiftUIコードが付属しています。
Kotlin Multiplatform Mobile pluginをインストールして、Android Studio IDE を使用し、プロジェクトを作成することもできます。
ComposeとSwiftUIがどのように連携するかをデモしてみます。ComposableコードをiOS に組み込むには、ComposableコードをComposeUIViewController
内にラップする必要があります。ComposeUIViewController
は UIKit からUIViewController
の値を返し、その中にComposeコードの組み立てをコンテンツパラメータとして含めることができます。
例:
// MainViewController.kt
fun ComposeEntryPoint(): UIViewController {
return ComposeUIViewController {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(text = "Hello from Compose")
}
}
}
次に、この関数を iOS 側から呼び出します。そのためには、SwiftUI のComposeコードを表す構造が必要です。以下のコードは、共有モジュールであるUIViewController コードを SwiftUI ビューに変換します。
// ComposeViewControllerRepresentable.swift
struct ComposeViewControllerRepresentable :UIViewControllerRepresentable {
func updateUIViewController(_ uiViewController:UIViewControllerType, context:Context) {}
func makeUIViewController (context:Context)-> some UIViewController {
return MainViewControllerKt.ComposeEntryPoint()
}
}
ここで、MainViewControllerKt.ComposeEntryPoint()
の名前を詳しく見てみましょう。これが Kotlin から生成されたコードになります。そのため、共有モジュール内のファイル名とコードによって異なる場合があります。共有モジュール内のファイル名が Main.ios.kt
で、UIViewController
returning function nameが ComposeEntryPoint()
の場合、Main_iosKt.ComposeEntryPoint()
のように呼び出す必要があります。そのため、コードによって異なります。
次に、この ComposeViewControllerRepresentable
をコードContentView()
の内部からインスタンス化します。これで準備は完了です。
// ContentView.swift
struct ContentView:View {
var body: some View {
composeViewControllerRepresentable ()
.ignoresSafeArea (.all)
}
}
コードを見てわかるように、このComposeコードは SwiftUI 内のどこでも使用でき、SwiftUI 内で好きなようにサイズを制御できます。UI は次のようになります:
SwiftUI
のコードをCompose内に統合したい場合は、UIView
でラップする必要があります。SwiftUIのコードをKotlinで直接記述することはできないため、Swiftで記述してKotlin関数に渡す必要があります。これを実装するために、 関数ComposeEntryPoint()
に、 UIView タイプの引数を追加してみましょう。
// 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),
)
}
}
}
そして CreateUIView を以下のような Swift コードへ渡します。
// ComposeViewControllerRepresentable.swift
struct ComposeViewControllerRepresentable : UIViewControllerRepresentable {
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {}
func makeUIViewController(context: Context) -> some UIViewController {
return MainViewControllerKt.ComposeEntryPoint(createUIView: { () -> UIView in
UIView()
})
}
}
さて、他のViewを追加したい場合は、以下のように親ラッパー UIView
を作成してください:
// 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")
}
}
次に、それをComposeViewControllerRepresentable
に追加し、必要に応じてViewを追加します。
// 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)
})
})
}
出力は次のようになります:
この方法では、共有の合成可能なコードに、好きなだけSwiftUIコードを追加できます。
また、UIKit
コードをCompose内に統合したい場合、中間コードを自分で作成する必要はありません。Compose Multiplatformが提供するComposable関数UIKitView ()
を使用して、その中にUIKitコードを直接追加できます。
// MainViewController.kt
UIKitView(
modifier = Modifier.fillMaxWidth().height(350.dp),
factory = { MKMapView() }
)
このコードは iOS ネイティブのマップ画面をCompose内に統合します。
Gemni Chatアプリの実装
それでは、ComposeコードをSwiftUI内に統合して、Gemini Chat アプリの実装を進めましょう。Jetpack Compose の LazyColumn
を使用して、基本的なチャット UI を実装します。Compose Multiplatform内にSwiftUIを統合することが主な目的なので、Composeやデータ、ロジック等、他の部分の実装についてはここでは割愛します。Gemini Pro APIを実装するため、我々はKtorネットワーキングライブラリを利用しました。Ktorの実装についての詳細は、Creating a cross-platform mobile application のページをご覧ください。
このプロジェクトでは、Compose Multiplatformで全てのUIを実装しました。Compose MultiplatformのTextFieldではiOS側でパフォーマンスに問題があるので、iOSアプリの入力フィールドにのみSwiftUIを使用します。
ComposeEntryPoint()
関数の中にComposeコードを入れてみましょう。これらのコードには、TopAppBarを含むチャットUIとメッセージのリストが含まれています。これには、Androidアプリで使用される入力フィールドの条件付き実装もあります。
// MainViewController.kt
fun ComposeEntryPoint(): UIViewController =
ComposeUIViewController {
Column(
Modifier
.fillMaxSize()
.windowInsetsPadding(WindowInsets.systemBars),
horizontalAlignment = Alignment.CenterHorizontally
) {
ChatApp(displayTextField = false)
}
}
false
を displayTextField
に渡したので、iOS バージョンのアプリでは Compose 入力フィールドがアクティブになりません。そして、Android側のTextFieldにはパフォーマンスの問題がないため、Android 実装側からComposable関数をこの ChatApp ()
のComposable関数を呼び出すと、displayTextField
の値は true
で返ってきます。(これはAndroid のネイティブ UI コンポーネントです。)
それでは、Swift コードに戻ってSwiftUIで入力フィールドを実装します。
// TextInputView.swift
struct TextInputView: View {
@Binding var inputText: String
@FocusState private var isFocused: Bool
var body: some View {
VStack {
Spacer()
HStack {
TextField("メッセージを入力する...", 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)
}
}
}
そして、 ContentView
構造体に戻り、以下のように修正します:
// 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)
}
}
}
ここでは ZStack
を追加し、その中にTopGradientカラーと、Modifier ignoresSafeArea ()
を追加して、ステータスバーの色が他の UI の色と一致するようにしました。
次に、共有されたCompose コードのラッパー ComposeViewControllerRepresentable
を追加し、メインのチャットUIを実装しました。そして、TextInputView()
というSwiftUIビューも追加しました。これにより、iOSアプリのユーザーにもiOSネイティブコードでスムーズなパフォーマンスが提供することができます。最終的なUIは次のようになります。
Gemini Chat iOS | Gemini Chat Android |
---|---|
![]() |
![]() |
ここでは、ChatAppのUIコード全体がKMPのCompose MultiplatformでAndroidとiOSの両方に共有され、iOSの入力フィールドのみがSwiftUIにネイティブに統合されています。
このプロジェクトの完全なソースコードは、GitHub で公開リポジトリとして公開されています。
GitHubリポジトリ:Compose MultiplatformにおけるSwiftUI
さいごに
このように、Kotlin MultiplatformとCompose Multiplatformを使うことで、クロスプラットフォームアプリでのパフォーマンスの問題を解決しながら、ユーザーにネイティブのような操作感と外観を提供できます。また、プラットフォーム間でコードを好きなだけ共有できるため、開発コストも削減できます。Compose Multiplatformでは、デスクトップアプリとコードを共有することもできます。ですから、単一のコードベースをデスクトップアプリだけでなくモバイルプラットフォームでも使用できます。さらに、プラットフォーム間でのコードベース共有を促進するため、Webサポートも進行中です。Kotlin Multiplatform (KMP) のもう1つの大きな利点は、コードを無駄にすることなく、いつでもネイティブ開発に切り替えることができる点です。AS-ISのKMPコード はAndroidネイティブのため、Androidアプリではそのまま利用でき、iOSアプリを別途切り離して開発することができます。また、KMPにすでに実装したものと同じSwiftUIコードを再利用することも可能です。このフレームワークは、高性能のアプリケーションを提供するだけでなく、共有するコードの割合を自由に変更したり、ネイティブ開発にいつでも切り替えたりできます。
本記事はここまでとしますが、KINTOテクノロジーズのテックブログでは今後もおもしろい記事を発信していきます!Happy Coding!
関連記事 | Related Posts

SwiftUIをCompose Multiplatformで使用する

Mobile App Development Using Kotlin Multiplatform Mobile (KMM) and Compose Multiplatform

Mobile App Development Using Kotlin Multiplatform Mobile (KMM)

Kotlin Multiplatform Mobile(KMM)およびCompose Multiplatformを使用したモバイルアプリケーションの開発

Using Combine to Achieve MVVM

Kotlin Multiplatform Mobile (KMM)を使ったモバイルアプリ開発
We are hiring!
【プロダクト開発バックエンドエンジニア(リーダークラス)】共通サービス開発G/東京・大阪
共通サービス開発グループについてWebサービスやモバイルアプリの開発において、必要となる共通機能=会員プラットフォームや決済プラットフォームなどの企画・開発を手がけるグループです。KINTOの名前が付くサービスやKINTOに関わりのあるサービスを同一のユーザーアカウントに対して提供し、より良いユーザー体験を実現できるよう、様々な共通機能や顧客基盤を構築していくことを目的としています。
【UI/UXデザイナー】クリエイティブ室/東京・大阪
クリエイティブ室についてKINTOやトヨタが抱えている課題やサービスの状況に応じて、色々なプロジェクトが発生しそれにクリエイティブ力で応えるグループです。所属しているメンバーはそれぞれ異なる技術や経験を持っているので、クリエイティブの側面からサービスの改善案を出し、周りを巻き込みながらプロジェクトを進めています。