KINTO Tech Blog
KMP

Compose MultiplatformとSwiftUIで作るハイブリッドモバイルアプリ(Part 3):SwiftUI連携と技術Tips

Cover Image for Compose MultiplatformとSwiftUIで作るハイブリッドモバイルアプリ(Part 3):SwiftUI連携と技術Tips

この記事は KINTOテクノロジーズアドベントカレンダー2025 の22日目の記事です🎅🎄

はじめに

こんにちは、KINTOテクノロジーズ Mobile Assistanceマネージャー&KMPチームリードのYena Hwangです。

本記事は「Compose MultiplatformとSwiftUIで作るハイブリッドモバイルアプリ」シリーズの最終回Part 3です。Part 1ではハイブリッド開発を選んだ背景を、Part 2では具体的な実装方法を紹介しました。今回はSwiftUI連携の詳細、CMP vs Flutter比較、そして実践で直面した落とし穴と導入戦略を解説します。

本シリーズの構成

  1. Part 1:なぜハイブリッドなのか
  2. Part 2:実装ガイド
  3. Part 3:SwiftUI連携と技術Tips ← 現在の記事

SwiftUIとCMPの相互埋め込み

ハイブリッドアーキテクチャの核心は、SwiftUIとCompose Multiplatform(CMP)を自由に組み合わせられることです。ここでは、共通モジュールを実際にネイティブ画面に、どう統合するのかをご紹介します。

SwiftUI + CMP統合

ComposeをSwiftUIに埋め込む

SwiftUIにComposeを埋め込む方法です。

SwiftUI内にComposeを埋め込んだ画面

まず、CMP側ではUIViewControllerを返します。

// AppComposeUI.kt
fun createProfileVC(
    user: User
): UIViewController {
    return ComposeUIViewController {
        ...
    }
}

そしてSwiftUI側ではUIViewControllerRepresentableを使って、そのコントローラをラップします。

// ComposeViewContainer.swift
import SwiftUI
import shared  // Import the shared KMP framework

struct ComposeViewContainer: UIViewControllerRepresentable {
    let user: User

    func makeUIViewController(context: Context) -> UIViewController {
        // Use the Compose wrapper to create the UIViewController
        return AppComposeUIKt.createProfileVC(user)
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
    }
}

これでSwiftUIの中にCMPの画面が自然に表示されます。

以下はComposeViewContaineruserを渡している例です。

// ContentView.swift
struct ContentView: View {
    let user: User

    var body: some View {
        VStack {
            Text("SwiftUI Header").font(.headline).padding()
            // Embed the Compose UI inside SwiftUI.
            ComposeViewContainer(user: user)
                .frame(height: 250)
            List {
                Text("SwiftUI Item 1")
                Text("SwiftUI Item 2")
            }
        }
    }
}

このように書くだけで、CMPの画面をそのままSwiftUIのコンポーネントとして扱えるようになります。

SwiftUIをComposeに埋め込む

今回は、逆に、SwiftUIをComposeに埋め込む方法です。

Compose内にSwiftUIを埋め込んだ画面

まずKotlin(iosMain)側にエントリーポイントを用意します。SwiftUIをComposeに埋め込むための入口です。KotlinはSwiftUIを生成しません。Swiftから() -> UIViewControllerのファクトリを受け取ります。

// ComposeEntryPointWithUIViewController.kt
fun ComposeEntryPointWithUIViewController(
    createUIViewController: () -> UIViewController
): UIViewController = ComposeUIViewController {
    Column(
        Modifier
            .fillMaxSize()
            .windowInsetsPadding(WindowInsets.systemBars),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("Embedding SwiftUI into Compose")
        UIKitViewController(
            factory = createUIViewController,
            modifier = Modifier.size(250.dp).border(2.dp, Color.Blue),
        )
    }
}

そしてCompose側ではUIKitViewController()を使って、SwiftUIビューを表示します。

次にSwift側の実装です。ここではMySwiftUIViewを定義して、UIHostingControllerでラップします。

// MySwiftUIView.swift
struct MySwiftUIView: View {
    let userName: String

    var body: some View {
        VStack(spacing: 8) {
            Text("Native SwiftUI Content").font(.headline)
            Text("Welcome, \(userName)")
        }
    }
}

// Pass SwiftUI to Kotlin entry point
struct ComposeViewControllerRepresentable: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> UIViewController {
        // Pass a factory to Kotlin that returns a UIViewController.
        // Here we wrap our SwiftUI view in a UIHostingController.
        return Main_iosKt.ComposeEntryPointWithUIViewController(createUIViewController: {
            UIHostingController(rootView: MySwiftUIView(userName: "Yena Hwang"))
        })
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}

そして、そのコントローラをComposeEntryPointWithUIViewControllerにファクトリとして渡します。このようにして、結果的にSwiftUIの画面をComposeの中に表示することができます。

ネイティブUIコンポーネントの埋め込み:MapView

次に、ネイティブUIコンポーネントを利用した例です。CMP側にネイティブUIコンポーネントを埋め込んだアプリの動作をご覧ください。このMapViewはiOSのネイティブコンポーネントです。

ネイティブMapViewを埋め込んだ画面

// MapContainer.kt
// commonMain
@Composable
expect fun MapContainer(
    modifier: Modifier = Modifier,
    dealers: List<Dealer>
)

// iosMain
@Composable
actual fun MapContainer(
    modifier: Modifier,
    dealers: List<Dealer>
) {
    UIKitView(
        factory = { // platform.MapKit
            MKMapView(frame = CGRectZero.readValue()).apply {
                showsUserLocation = true
                delegate = MapDelegate(
                    ...
                )
            }
        }
    )
}

共通コードではexpect関数を宣言し、iOS側ではactualを実装しています。そしてUIKitViewを使って、MKMapViewをそのまま埋め込んでいます。

このように、expect/actualパターンを使うことで、共通インターフェースを維持しながらプラットフォーム固有のネイティブコンポーネントを自由に埋め込むことができます。

CMP vs Flutter:なぜCMPを選んだのか

ここまでCMPとネイティブUIの使い分けについて見てきましたが、実は他のクロスプラットフォームのフレームワークでも同じような課題があります。

ネイティブビュー統合方式の決定的な違い

例えばFlutterの公式ドキュメントには、『Platform Viewにはパフォーマンス面でトレードオフがある』と明記されています。

区分 CMP Flutter
レンダリング Inline Rendering 😄 Overlay 🥲
特徴 高速で効率的 重いビューでカクつき

技術的な違い

CMPとFlutterでは、ネイティブUIと埋め込みUIの相互運用方式が異なり、パフォーマンスに大きな差が出ます。

Host UI → Embedded UI Rendering path Performance impact
Native → CMP Android: AndroidView inside Compose (same View system)
iOS: CMP Skia surface hosts a UIKit view via UIKitView
Very Low (Android) / Low–Moderate (iOS)
CMP → Native Android: ComposeView in a ViewGroup (Compose runs on platform renderer)
iOS: SwiftUI/UIViewController hosts ComposeUIViewController (Skia surface inside)
Low (Android) / Moderate (iOS)
Native → Flutter Flutter renders via Skia/Impeller; native view injected as PlatformView (texture/layer composition) Moderate; visible stutter on heavy views (Map, WebView)
Flutter → Native Native Activity/VC hosts Flutter engine surface Extra startup cost (engine warm-up), stable after warm-up

CMP vs Flutter比較

CMPの強み

  • ネイティブMapViewをそのまま使用し、ネイティブAPIでアイコン/経路レンダリング
  • MKMapViewなどのネイティブコンポーネントを直接配置
  • コンポーネント単位で交換可能
  • ネイティブパフォーマンスとアクセシビリティ維持

ネイティブMapView統合

複雑なハイブリッドUIパフォーマンス比較

Framework Startup Time Memory Usage UI Smoothness
Native ⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐
CMP + Native ⭐⭐⭐ ⭐⭐ ⭐⭐⭐
Flutter + Native ⭐⭐ ⭐⭐ ⭐⭐

実践で直面した落とし穴

"Every solution breeds new problems." — Arthur Bloch

ここまでCMPとネイティブUIの組み合わせの利点を見てきましたが、どんな技術にも課題はあります。私たちがKMP/CMP開発で実際に直面した落とし穴を紹介します。

KMPロジックレイヤー

1. HTTP/TLS/リダイレクト/クッキー

  • 症状:プラットフォーム別エンジンが異なり、クッキー永続性、リダイレクト、TLSピニングが異なる動作
  • 解決:タイムアウト/キャッシュ/ロギング共有設定 + DIやexpect/actualでプラットフォーム別エンジン+セキュリティポリシー注入

2. 正規表現の違い

  • 症状:JVMはjava.util.regex、iOS/Nativeは ICUベースエンジン使用。\R、可変幅lookbehindなどがiOSで異なる動作またはサポートなし
  • 解決:複雑な機能を避ける、シンプルなサブパターンに分離、エンジン交換可能なアダプター構成

3. タイムゾーン & DST

  • 症状:プラットフォーム別タイムゾーンDBとフォーマッティング規則が異なる
  • 解決:すべての計算はkotlinx-datetimeで、フォーマッティングはexpect/actualでプラットフォームに委任

CMP UIレイヤー

1. フォント & タイポグラフィ(CJK/絵文字)

  • 症状:プラットフォーム別フォールバックが異なり、行の高さ、省略、絵文字幅が異なる
  • 解決:フォントバンドル、フォントファミリー/ウェイト明示的宣言、lineHeight/letterSpacing明示的設定

2. 省略/テキスト測定の違い

  • 症状:iOS/SkikoテキストレイアウトがAndroidとピクセル単位で一致しない
  • 解決:maxLines + TextOverflow.Ellipsis使用、「ぴったり」な仮定を避ける、主要画面スクリーンショットリグレッションテスト

3. スクロール & ジェスチャー物理

  • 症状:iOSの慣性がより「滑らか」で、ネストスクロールの感触が異なる。また、CMPのScrollの中にネイティブのScrollを入れると、内側のScrollが動作しない問題もある
  • 注意:スクロール物理設定を抽象化してプラットフォーム別分岐、複雑なネストスクロールページは単純化。Scroll ViewのUXは設計段階から調整が必要

導入戦略:プロダクト段階別アプローチ

CMP/KMPの真の強みは柔軟性です。共有コードとネイティブコードの比率を自由に調整できるため、プロトタイプでは再利用を最大化し、プロダクトが成熟したらネイティブ比率を高めることができます。

導入戦略

新規プロダクト:共有比率を高くスタート

段階 共有比率 目標 リソース状況
Start Stage ~99% 迅速なリリース、アイデア検証 限られた開発リソース
Growth Stage ~80% プロダクト拡張 + ネイティブUX強化 標準開発リソース
Maturity Stage ~50% パフォーマンス最適化、長期保守性、チーム専門化 豊富な開発リソース

既存プロダクト:段階的導入

1. Start Small(1%)
最も安全なエントリーポイント。最も小さくリスクの低い領域にKMP/CMP導入

2. Stepwise Expansion(20%)
安定性と確信を確保しながら導入範囲を拡張

3. End Stage(~50%)
共有コードとネイティブコードの持続可能なバランスポイントに到達

おわりに

ハイブリッドアーキテクチャを導入して得られた成果をまとめると:

項目 Before(従来・ネイティブ) After(ハイブリッド)
開発効率 両OS別々に実装 50%削減、一箇所でバグ修正
コスト 重複作業・すり合わせ必要 コミュニケーション・QA・保守削減
チーム OS別に専門エンジニア必要 少人数で両プラットフォーム対応
品質 ネイティブ品質 ネイティブ品質を維持しながら共有
柔軟性 ネイティブのみ 必要な部分だけ共有可能

ネイティブの強みを活かしながらコードを共有できる——これがハイブリッド戦略の最大のメリットでした。

DroidKaigiで発表しながら、多くの方々が同じ悩みを持っていることを感じました。これからKMP・CMPの導入を検討しながら、両OSのネイティブコードも活かしたハイブリッドアーキテクチャを準備している方々に、私たちのチームの経験が少しでも参考になれば幸いです。

重複作業を減らし、両OSに効率よく対応しながら、高品質なアプリを素早くユーザーに届ける——KMP・CMPを活用したハイブリッド開発を、ぜひ検討してみてください。


こちらは「Compose MultiplatformとSwiftUIで作るハイブリッドモバイルアプリ」シリーズのPart 3(最終回)です。

Facebook

関連記事 | Related Posts

We are hiring!

【プロジェクトマネージャー(iOS/Android/Flutter)】モバイルアプリ開発G/東京

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

【ソフトウェアエンジニア(メンバークラス)】共通サービス開発G/東京・大阪・福岡

共通サービス開発グループについてWebサービスやモバイルアプリの開発において、必要となる共通機能=会員プラットフォームや決済プラットフォームなどの企画・開発を手がけるグループです。KINTOの名前が付くサービスやKINTOに関わりのあるサービスを同一のユーザーアカウントに対して提供し、より良いユーザー体験を実現できるよう、様々な共通機能や顧客基盤を構築していくことを目的としています。

イベント情報