Compose Multiplatformでテーマに同期してStatusBarのアイコン色を変更する

Compose Multiplatformでテーマに同期してStatusBarの色を変えたい場合はAndroidやiOSのOSごとに処理を記述する必要があります。今回はAndroidとiOSでStatusBarをテーマに同期して、アイコン色を変更する方法について調べたのでまとめていきます。

Table Of Content

テーマの変更を検知できるようにする

まず初めにテーマの変更を検知できるように、共通化しているAppを変更していきます。以下のようにApponChangeDarkModeを定義して、Appが保持しているisDarkを変更時に、通知できるようにします。

App.kt
@Composable
internal fun App(onChangeDarkMode: (Boolean) -> Unit) {
    var isDark by remember { mutableStateOf(false) }
    MaterialTheme(colorScheme = if (isDark) darkColorScheme() else lightColorScheme()) {
        Surface {
            Box(
                modifier = Modifier.fillMaxSize(),
                contentAlignment = Alignment.Center
            ) {
                IconButton(
                    onClick = {
                        isDark = !isDark
                        onChangeDarkMode(isDark)
                    }
                ) {
                    Icon(
                        modifier = Modifier.size(64.dp),
                        imageVector = if (isDark) Icons.Default.LightMode else Icons.Default.DarkMode,
                        contentDescription = null
                    )
                }
            }
        }
    }
}

このようにonChangeDarkModeを定義して、isDarkの変更を通知するようにすることで、各OSのApp.android.ktApp.ios.ktでテーマ変更を検知して、StatusBarの色を変更する処理を動作させることが可能になります。

AppActivity
class AppActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            App(onChangeDarkMode = { /** StatusBarの色を変更する処理 */ })
        }
    }
}
MainViewController
fun MainViewController(): UIViewController = ComposeUIViewController {
    App(onChangeDarkMode = {  /** StatusBarの色を変更する処理 */ })
}

AndroidでStatusBarのアイコン色を変更できるようにする

Compose for AndroidでStatusBarのアイコン色を変更するには、まずActivityWindowを取得してstatusBarColornavigationBarColorを変更します。その次にWindowsCompatInsetsControllerを取得して、isAppearanceLightStatusBarsisAppearanceLightNavigaitonBarsを変更します。このコードで通知されたisDarkの状態に応じてStatusBarのアイコン色を変更ができます。

AppActivity
class AppActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            App(onChangeDarkMode = { changeStatusBarColor(it) })
        }
    }

    private fun changeStatusBarColor(isDark: Boolean) {
        val window = this.window
        val systemBarColor = if (isDark) Color.BLACK else Color.WHITE
        window.statusBarColor = systemBarColor
        window.navigationBarColor = systemBarColor
        WindowCompat.getInsetsController(window, window.decorView).apply {
            isAppearanceLightStatusBars = !isDark
            isAppearanceLightNavigationBars = !isDark
        }
    }
}

iOSでStatusBarの色を変更できるようにする

Compose for iOSはUIKitのUIViewControllerを継承したComposeUIViewControllerを作成して、このComposeUIViewControllerをSwiftUI上に埋め込むことで、Compose Multiplatformで記述したUIをSwiftUIアプリ上に表示するという仕組みになっています。

  • SwiftUIのAppプロトコルはアプリ画面の構造(ルート)を定義するもの
  • SwiftUIのViewプロトコルはアプリ画面の1つの要素を定義するもの
  • SwiftUIのUIViewControllerRepresentableプロトコルはUIKitのUIViewController / UIViewをSwiftUIで利用可能にする
  • 以下のコードではUIViewControllerRepresentableプロトコルを通じて、作成したComposeUIViewControllerつまりUIViewControllerをSwiftUIのViewとして埋め込んでいる。
main.kt(iOS)
fun MainViewController(): UIViewController  = ComposeUIViewController { App() }
main.swift
@main
struct iosApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

struct ContentView: View {
    var body: some View {
        ComposeView().ignoresSafeArea(.all)
    }
}

struct ComposeView: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> UIViewController {
        MainKt.MainViewController()
    }

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

というようにCompose Multiplatform for iOSではUIKitのUIViewControllerの仕組みを利用しているので、StatusBarのアイコン色を変更する場合は、ComposeUIViewController が継承しているUIViewControllerpreferredStatusBarStyleをカスタマイズしてアイコン色を変更します。

CustomUIViewController
class CustomUIViewController : ComposeUIViewController {
    override fun preferredStatusBarStyle(): UIStatusBarStyle {
        // UIStatusBarStyleDarkContentやUIStatusBarStyleLightContentを指定して、StatusBarの表示をどっちのテーマにするか決める
        return UIStatusBarStyleDarkContent
    }
}

なのですがComposeUIViewControllerは再継承できないようになっているため、直接的にpreferredStatusBarStyleをカスタマイズできない仕組みになっています。なので今回は以下のようなComposeUIViewControllerをラップするMainUIViewControllerを作成してpreferredStatusBarStyleをカスタマイズする方法で、通知されたisDarkの状態に応じてStatusBarのアイコン色を変更するようにします。

MainViewController
fun MainViewController(): UIViewController = MainUIViewController()
MainUIViewController
class MainUIViewController : UIViewController {
    @OverrideInit
    constructor() : super(nibName = null, bundle = null)

    @OverrideInit
    constructor(coder: NSCoder) : super(coder)

    /**
     * isDarkの状態を変更したら、StatusBarのアップデートを要求する
     */
    private var isDark: Boolean = false
        set(value) {
            field = value
            setNeedsStatusBarAppearanceUpdate()
        }

    /**
     * Appkから通知された、isDarkを内部に保存し、その時にStatusBarをアップデートする
     */
    private val childComposeViewController = ComposeUIViewController {
        App(onChangeDarkMode = { isDark -> this.isDark = isDark })
    }

    /**
     * isDarkの状態によって、StatusBarStyleが変化するようにする
     */
    override fun preferredStatusBarStyle(): UIStatusBarStyle {
        return if (isDark)  UIStatusBarStyleLightContent else UIStatusBarStyleDarkContent
    }

    override fun loadView() {
        super.loadView()

        // RootのUIViewControllerのViewをセットする
        view = UIView().apply {
            // SubViewとしてComposeのUIViewControlerのViewをセットする
            addSubview(
                childComposeViewController.view.apply {
                    // AutoresizingMaskを利用し、RootのViewに合わせてリサイズするようにする
                    setAutoresizingMask(
                        UIViewAutoresizingFlexibleWidth or UIViewAutoresizingFlexibleHeight
                    )
                }
            )
        }

        // RootのUIViewControllerの子にComposeのUIViewControllerを追加する
        addChildViewController(childComposeViewController)
        childComposeViewController.didMoveToParentViewController(this)
    }
}

動作確認してみる

AndroidとiOSで動作確認をしてみます、すると以下のようにStatusBarのアイコン色が変更できています。本記事では細かなコードの説明は省いていますので、このような動作にならない場合は下記のリポジトリのコードを見ていただければと思います。

https://github.com/kaleidot725/Compose-Multiplatform-Playground/tree/main/projects/safeAreaIcon

iOS
Android

参考文献

上記のコードは以下のページを参考に作成しています。