🫗

Compose MultiplatformでLiquid glass対応のタブ表示を行うには

に公開

こんにちは!sugitaniと申します。ブラックキャット・カーニバル(略称ブラキャニ)というCompose Multiplatformで作られたSNSアプリを開発しています。

本稿はブラキャニの開発で得られた"あれどうやるんだっけ" を備忘録も兼ねて共有していくシリーズの7作目です。

Liquid glass対応のタブ表示を行う

公式ドキュメントにも説明がありますが、Compose MultiplatformはUIKitと相互乗り入れが可能であり、ComposeのコンポーネントをUIViewControllerに変換することができます。

これを利用してUITabBarControllerにComposeのコンポーネントを表示してもらい、iOS26で動かせばLiquid glassでのタブ移動ができます。

公式ドキュメントとほぼ同じ内容になりますが、やってみましょう

下準備

タブで表示させたい中身を作ってみましょう

composeApp/src/commonMain/kotlin/cc/bcc/cmpexamples/example007/App.kt
package cc.bcc.cmpexamples.example007

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp

enum class PetType(
    val emoji: String,
    val bgGradient: List<Color>,
) {
    CAT(
        "🐱",
        listOf(
            Color(0xFFFFC107),
            Color(0xFFFF9800),
        ),
    ),
    DOG(
        "🐶",
        listOf(
            Color(0xFF4CAF50), // 明るい緑
            Color(0xFF2E7D32), // 濃い緑
        ),
    ),
}

@Composable
fun PetIconComponent(
    pet: PetType,
    modifier: Modifier = Modifier,
) {
    Box(
        modifier =
            modifier.fillMaxSize().background(
                brush =
                    Brush.verticalGradient(
                        colors = pet.bgGradient,
                    ),
            ),
        contentAlignment = Alignment.Center,
    ) {
        Box(
            modifier = Modifier.size(100.dp).background(Color.White, shape = CircleShape),
            contentAlignment = Alignment.Center,
        ) {
            Text(
                text = pet.emoji,
                fontSize = 64.sp,
            )
        }
    }
}

Androidで利用

まず普通に使うとどうなるかAndroid側を実装して確認します

composeApp/src/androidMain/kotlin/cc/bcc/cmpexamples/example007/App.android.kt
package cc.bcc.cmpexamples.example007

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Pets
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.Color

class AppActivity : ComponentActivity() {
    @OptIn(ExperimentalMaterial3Api::class)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            var selectedTabIndex by remember { mutableIntStateOf(0) }

            MaterialTheme {
                Scaffold(
                    bottomBar = {
                        NavigationBar(containerColor = Color.Transparent) {
                            NavigationBarItem(
                                icon = { Icon(Icons.Default.Pets, contentDescription = null) },
                                label = { Text("Cat") },
                                selected = selectedTabIndex == 0,
                                onClick = { selectedTabIndex = 0 },
                            )

                            NavigationBarItem(
                                icon = { Icon(Icons.Default.Pets, contentDescription = null) },
                                label = { Text("Dog") },
                                selected = selectedTabIndex == 1,
                                onClick = { selectedTabIndex = 1 },
                            )
                        }
                    },
                ) {
                    when (selectedTabIndex) {
                        0 -> PetIconComponent(PetType.CAT)
                        1 -> PetIconComponent(PetType.DOG)
                    }
                }
            }
        }
    }
}

iOSで利用

iOSで使うために、まずUIViewControllerに変換するヘルパー関数を作成します

composeApp/src/iosMain/kotlin/main.kt
@file:Suppress("unused", "FunctionName")

import androidx.compose.ui.window.ComposeUIViewController
import cc.bcc.cmpexamples.example007.PetIconComponent
import cc.bcc.cmpexamples.example007.PetType
import platform.UIKit.UIViewController

fun CatViewController(): UIViewController =
    ComposeUIViewController { PetIconComponent(PetType.CAT) }

fun DogViewController(): UIViewController =
    ComposeUIViewController { PetIconComponent(PetType.DOG) }

次にSwift側で以下のように使います

iosApp/iosApp/iosApp.swift
import ComposeApp
import SwiftUI

@main
struct ComposeApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView().ignoresSafeArea(.all)
        }
    }
}

struct ContentView: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> UIViewController {

        let catViewController = MainKt.CatViewController()
        catViewController.title = "The Cat"

        let dogViewController = MainKt.DogViewController()
        dogViewController.title = "The Dog"

        // Set up the UITabBarController
        let tabBarController = UITabBarController()
        tabBarController.viewControllers = [
            // Wrap them in a UINavigationController for the titles
            UINavigationController(rootViewController: catViewController),
            UINavigationController(rootViewController: dogViewController),
        ]
        tabBarController.tabBar.items?[0].title = "Cat"
        tabBarController.tabBar.items?[0].image = UIImage(systemName: "cat")

        tabBarController.tabBar.items?[1].title = "Dog"
        tabBarController.tabBar.items?[1].image = UIImage(systemName: "dog")

        // Suspected iOS 26.0 beta bug: label rendering is incorrect until tab is switched, so forcing selection
        tabBarController.selectedIndex = 1
        tabBarController.selectedIndex = 0

        return tabBarController
    }

    func updateUIViewController(
        _ uiViewController: UIViewController,
        context: Context
    ) {
        // Updates will be handled by Compose
    }
}

素直に

  1. タブの中身となるUIViewControllerを作成する
  2. UITabBarControllerを作り、VCを積載する
  3. UITabBarControllerを表示してもらうようにする

としているだけです

ダイアログなどもプラットフォームネイティブで表示するには?

https://github.com/MohamedRejeb/Calf

Calfを利用すると Dialog / BottomSheet / 読み込みIndicator / DataPicker / TimePicker / WebView / File Picker / 権限リクエストなどがネイティブUIで利用出来ます。

サンプルプロジェクト

本稿のソースコード、および動作するコードは
https://github.com/blackcat-carnival/cmp-examples/tree/main/007.use_platform_ui
にあります。

免責事項

このコードはあくまで"自分はこう実装した"という例ですので、よりよい方法がある可能性があります。見つけたら教えてください!

以下宣伝

ブラックキャット・カーニバル

Discussion