この記事の目的
前回の記事 でとにかくCompose Multiplatformを触れるようになるところまでを書きました。
その次となる本記事では、Compose Multiplatform がどのようにして iOS の UI を Kotlin で表現しているのかを探り、「なんとなく仕組みは分かった」と言えるようになるまでを目的とします。
前提
Compose Multiplatform for iOS はこの記事執筆時点(2023/06/29)でAlpha版です。
仕組みが大きく変更される可能性もありうるので、あくまでこの時点ではこんな仕組みなんだ(だったんだ)、という程度でお読みください。
アプリケーションとライブラリのコードを見る
まずは前回の記事でも利用した、Compose Multiplatformアプリケーションのテンプレートプロジェクトのコードを見てみます。
Androidアプリのルートとなるファイルの中身はこんな感じ。
import androidx.compose.runtime.Composable
actual fun getPlatformName(): String = "Android"
@Composable fun MainView() = App()
本題からずれてしまうので簡単な紹介に留めますが、App()
というのはテンプレートとして用意されている Hello, World!
なアプリケーションのルート画面です。
App()
の簡単な中身はこちらを展開
@OptIn(ExperimentalResourceApi::class)
@Composable
fun App() {
MaterialTheme {
var greetingText by remember { mutableStateOf("Hello, World!") }
var showImage by remember { mutableStateOf(false) }
Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
Button(onClick = {
greetingText = "Hello, ${getPlatformName()}"
showImage = !showImage
}) {
Text(greetingText)
}
AnimatedVisibility(showImage) {
Image(
painterResource("compose-multiplatform.xml"),
null
)
}
}
}
}
expect fun getPlatformName(): String
ちなみに、expect
と actual
というキーワードは Multiplatform 専用のインターフェイスとして使われています。少し違いますが、abstract
と override
のような関係だと理解しています。
DesktopもAndroidと同様。
import androidx.compose.runtime.Composable
actual fun getPlatformName(): String = "Desktop"
@Composable fun MainView() = App()
最後に本題のiOSですが、App()
が何者かに包まれています。
import androidx.compose.ui.window.ComposeUIViewController
actual fun getPlatformName(): String = "iOS"
fun MainViewController() = ComposeUIViewController { App() }
キーワードだけ見るとSwiftなんじゃないかと思うようなコードですね。
ComposeUIViewController
が何者かというと、compose-multiplatform-core に定義されていました。
実体としてはComposeWindow
というクラスが利用されており、skiaLayer
をレイヤーに持つSkikoUIView
上にcontent()
、すなわちテンプレートアプリケーションから渡したApp()
が描画されるようです。
// コードは大幅に省略しています
fun ComposeUIViewController(content: @Composable () -> Unit): UIViewController =
ComposeWindow().apply {
setContent(content)
}
@OptIn(InternalComposeApi::class)
@ExportObjCClass
internal actual class ComposeWindow : UIViewController {
private lateinit var layer: ComposeLayer
private lateinit var content: @Composable () -> Unit
override fun loadView() {
val skiaLayer = createSkiaLayer()
val skikoUIView = SkikoUIView(
skiaLayer = skiaLayer,
pointInside = { point, _ ->
!layer.hitInteropView(point, isTouchEvent = true)
},
).load()
val rootView = UIView() // rootView needs to interop with UIKit
rootView.backgroundColor = UIColor.whiteColor
skikoUIView.translatesAutoresizingMaskIntoConstraints = false
rootView.addSubview(skikoUIView)
//...
layer = ComposeLayer(
layer = skiaLayer,
platform = uiKitPlatform,
getTopLeftOffset = ::getTopLeftOffset,
input = uiKitTextInputService.skikoInput,
)
layer.setContent(content = {
CompositionLocalProvider(
LocalLayerContainer provides rootView,
LocalUIViewController provides this,
LocalKeyboardOverlapHeightState provides keyboardOverlapHeightState,
LocalSafeAreaState provides safeAreaState,
LocalLayoutMarginsState provides layoutMarginsState,
LocalInterfaceOrientationState provides interfaceOrientationState,
) {
content()
}
})
}
//...
actual fun setContent(
content: @Composable () -> Unit
) {
this.content = content
}
}
ここまで来れば、あとはSkia
とSkiko
が何者かが分かれば当初の
「なんとなく仕組みは分かった」と言えるようになる
という目的は果たせそうです。
SkiaとSkiko
Skia
はGoogleが開発している、C++製のクロスプラットフォームな2Dレンダリングライブラリです。多くのFlutterアプリでも利用されているライブラリで、Skiko
はSkia
をKotlinで扱えるようにしてくれるライブラリです。
そのため、(少なくとも現時点では)Compose Multiplatform for iOS は Flutter と同様の描画システムを持っている、と言えます。
Skia がやってくれていることについては、端的に言ってしまうとHTMLのCanvas APIのような形で画面上に座標ベースで画面要素をレンダリングします。
座標ベースでどんな形でも自由に描画することができるため、これを利用して「ボタンの形」や「テキストフィールドの形」を画面上に画面要素として描画します。
https://skia.org/docs/#showcase
ちなみに、Skia には致命的なパフォーマンス上の欠陥があり、Flutter は Skia の利用をやめて Impeller という別のレンダリングエンジンを利用するようにしていく方針のようです。
Compose Multiplatform for iOS も Impeller に乗り換えるのか、それとも Skia のままで正式リリースまで進むのか、今後の動向が気になりますね。
UIKitView
ここまでのUI共通化の流れと外れるのであまり触れませんでしたが、Alpha 版の Compose Multiplatform for iOS では UIKitView
というインターフェースで UIKit
や SwiftUI
との相互運用もサポートしています。
MKMapView
などの複雑な画面要素を Compose で実装された画面に組み込んだり、逆にSwiftUI
で実装された画面に Compose で実装された画面要素を組み込んだりすることも可能です。
参考にさせていただいたサイト