Compose Multiplatformでテーマに同期してStatusBarのアイコン色を変更する
Compose Multiplatformでテーマに同期してStatusBarの色を変えたい場合はAndroidやiOSのOSごとに処理を記述する必要があります。今回はAndroidとiOSでStatusBarをテーマに同期して、アイコン色を変更する方法について調べたのでまとめていきます。
テーマの変更を検知できるようにする
まず初めにテーマの変更を検知できるように、共通化しているApp
を変更していきます。以下のようにApp
にonChangeDarkMode
を定義して、App
が保持しているisDark
を変更時に、通知できるようにします。
@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.kt
やApp.ios.kt
でテーマ変更を検知して、StatusBarの色を変更する処理を動作させることが可能になります。
class AppActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
App(onChangeDarkMode = { /** StatusBarの色を変更する処理 */ })
}
}
}
fun MainViewController(): UIViewController = ComposeUIViewController {
App(onChangeDarkMode = { /** StatusBarの色を変更する処理 */ })
}
AndroidでStatusBarのアイコン色を変更できるようにする
Compose for AndroidでStatusBarのアイコン色を変更するには、まずActivity
のWindow
を取得してstatusBarColor
とnavigationBarColor
を変更します。その次にWindowsCompat
でInsetsController
を取得して、isAppearanceLightStatusBars
とisAppearanceLightNavigaitonBars
を変更します。このコードで通知されたisDark
の状態に応じてStatusBarのアイコン色を変更ができます。
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として埋め込んでいる。
fun MainViewController(): UIViewController = ComposeUIViewController { App() }
@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
が継承しているUIViewController
のpreferredStatusBarStyle
をカスタマイズしてアイコン色を変更します。
class CustomUIViewController : ComposeUIViewController {
override fun preferredStatusBarStyle(): UIStatusBarStyle {
// UIStatusBarStyleDarkContentやUIStatusBarStyleLightContentを指定して、StatusBarの表示をどっちのテーマにするか決める
return UIStatusBarStyleDarkContent
}
}
なのですがComposeUIViewController
は再継承できないようになっているため、直接的にpreferredStatusBarStyle
をカスタマイズできない仕組みになっています。なので今回は以下のようなComposeUIViewController
をラップするMainUIViewController
を作成してpreferredStatusBarStyle
をカスタマイズする方法で、通知されたisDarkの状態に応じてStatusBarのアイコン色を変更するようにします。
fun MainViewController(): UIViewController = 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
参考文献
上記のコードは以下のページを参考に作成しています。
- GitHub | InsetsX
- Apple Developer | UIViewController
- Apple Developer | preferredStatusBarStyle
- Android Developers | WindowInsetsController
- Android Developers | Window