Compose Multiplatformの仕組みを探る（iOS編）

## この記事の目的
[前回の記事](https://qiita.com/y-mimura/items/9ffdce14dc4ed6110343) でとにかくCompose Multiplatformを触れるようになるところまでを書きました。
その次となる本記事では、Compose Multiplatform がどのようにして iOS の UI を Kotlin で表現しているのかを探り、「なんとなく仕組みは分かった」と言えるようになるまでを目的とします。

## 前提
Compose Multiplatform for iOS はこの記事執筆時点（2023/06/29）でAlpha版です。
仕組みが大きく変更される可能性もありうるので、あくまでこの時点ではこんな仕組みなんだ（だったんだ）、という程度でお読みください。

## アプリケーションとライブラリのコードを見る
まずは前回の記事でも利用した、Compose Multiplatformアプリケーションのテンプレートプロジェクトのコードを見てみます。

https://github.com/JetBrains/compose-multiplatform-template

Androidアプリのルートとなるファイルの中身はこんな感じ。

```kotlin:shared/src/androidMain/kotlin/main.android.kt
import androidx.compose.runtime.Composable

actual fun getPlatformName(): String = "Android"

@Composable fun MainView() = App()
```

本題からずれてしまうので簡単な紹介に留めますが、`App()`というのはテンプレートとして用意されている `Hello, World!` なアプリケーションのルート画面です。

<details><summary><code>App()</code>の簡単な中身はこちらを展開</summary><div>


```kotlin:shared/src/commonMain/kotlin/App.kt
@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
```

</div></details>

ちなみに、`expect` と `actual` というキーワードは Multiplatform 専用のインターフェイスとして使われています。少し違いますが、`abstract` と `override` のような関係だと理解しています。

DesktopもAndroidと同様。

```kotlin:shared/src/desktopMain/kotlin/main.desktop.kt
import androidx.compose.runtime.Composable

actual fun getPlatformName(): String = "Desktop"

@Composable fun MainView() = App()
```

最後に本題のiOSですが、`App()`が何者かに包まれています。

```kotlin:shared/src/iosMain/kotlin/main.ios.kt
import androidx.compose.ui.window.ComposeUIViewController

actual fun getPlatformName(): String = "iOS"

fun MainViewController() = ComposeUIViewController { App() }
```

キーワードだけ見るとSwiftなんじゃないかと思うようなコードですね。
`ComposeUIViewController`が何者かというと、[compose-multiplatform-core](https://github.com/JetBrains/compose-multiplatform-core) に定義されていました。

実体としては`ComposeWindow`というクラスが利用されており、`skiaLayer`をレイヤーに持つ`SkikoUIView`上に`content()`、すなわちテンプレートアプリケーションから渡した`App()`が描画されるようです。

```kotlin:compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeWindow.uikit.kt
// コードは大幅に省略しています

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
    }
}
```

https://github.com/JetBrains/compose-multiplatform-core/blob/8f38e6eb9c412b38e807579794a43f4c4d02ba70/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeWindow.uikit.kt

ここまで来れば、あとは`Skia`と`Skiko`が何者かが分かれば当初の

> 「なんとなく仕組みは分かった」と言えるようになる  

という目的は果たせそうです。

## SkiaとSkiko

`Skia`はGoogleが開発している、C++製のクロスプラットフォームな2Dレンダリングライブラリです。多くのFlutterアプリでも利用されているライブラリで、`Skiko`は`Skia`をKotlinで扱えるようにしてくれるライブラリです。
そのため、（少なくとも現時点では）Compose Multiplatform for iOS は Flutter と同様の描画システムを持っている、と言えます。

Skia がやってくれていることについては、端的に言ってしまうとHTMLのCanvas APIのような形で画面上に座標ベースで画面要素をレンダリングします。
座標ベースでどんな形でも自由に描画することができるため、これを利用して「ボタンの形」や「テキストフィールドの形」を画面上に画面要素として描画します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/238072/189d10b0-8508-4c41-ffdc-6da70f4f9184.png" width="400" alt="Skia showcase" />
https://skia.org/docs/#showcase

ちなみに、Skia には致命的なパフォーマンス上の欠陥があり、Flutter は Skia の利用をやめて [Impeller](https://docs.flutter.dev/perf/impeller) という別のレンダリングエンジンを利用するようにしていく方針のようです。
Compose Multiplatform for iOS も Impeller に乗り換えるのか、それとも Skia のままで正式リリースまで進むのか、今後の動向が気になりますね。

## UIKitView
ここまでのUI共通化の流れと外れるのであまり触れませんでしたが、Alpha 版の Compose Multiplatform for iOS では `UIKitView` というインターフェースで `UIKit` や `SwiftUI` との相互運用もサポートしています。

https://blog.jetbrains.com/ja/kotlin/2023/05/compose-multiplatform-for-ios-is-in-alpha/

`MKMapView` などの複雑な画面要素を Compose で実装された画面に組み込んだり、逆に`SwiftUI` で実装された画面に Compose で実装された画面要素を組み込んだりすることも可能です。

## 参考にさせていただいたサイト

https://github.com/JetBrains/compose-multiplatform-core

https://blog.jetbrains.com/ja/kotlin/2023/05/compose-multiplatform-for-ios-is-in-alpha/

https://speakerdeck.com/ronnnnn/compose-multiplatformtutedounandarou-flutter-x-kotlin-multiplatform-by-cyberagent-number-7