kako.dev

開発、自作アプリのこと

Compose for iOS でiOSアプリを開発する

KotlinConfの内容を見ていたら、Compose for iOSのアルファ版がリリースされました。

github.com

私はAndroidエンジニアですが、ComposeでiOSアプリが作れるのはアツい展開なので早速動かしてみました。

Hello World

上記のリポジトリから「Use this templete」します。

その後環境をチェックしましょう。

Readmeに書かれている通りですが、kdoctorをinstallして環境が整っているかチェックします。

brew install kdoctor
kdoctor

動かすにはcocoapodsをインストールする必要があるんですが、M1などAppleシリコンのCPUだとちょっと一手間必要です。 詰まったらこのStackOverflowを見るように言われるのでこちらを参考に解決します。

ios - How to run CocoaPods on Apple Silicon (M1) - Stack Overflow

環境が整ったらAndroid Studioでrun対象をiosAppにしてビルドするとiOSシュミレーターが起動します。

少し遊んでみる

起動できたのでちょっとだけ遊んでみます。

このような料理の画像が一覧で見られるアプリを作ってみます。

moko-resourceライブラリの導入

画像を表示するにあたり今回moko-resource を利用しました。

github.com

リソースの扱いはプラットフォームごとに異なるだろうと想像はしていたんですが、その解決方法を探していたところ行きつきました。 Androidでのリソースの扱いと似た要領で、Compose Multiplatform 環境でiOSアプリでも画像を表示できます。

依存関係

まずはversion catalogで各アーティファクトを管理します。

[versions]
resourcesVersion = "0.21.2"

[libraries]
resources = { module = "dev.icerock.moko:resources", version.ref = "resourcesVersion" }
resourcesCompose = { module = "dev.icerock.moko:resources-compose", version.ref = "resourcesVersion" }
resourcesTest = { module = "dev.icerock.moko:resources-test", version.ref = "resourcesVersion" }

resourcesGradlePlugin = { module = "dev.icerock.moko:resources-generator", version.ref = "resourcesVersion" }

次はsetting.gradle.ktsに下記を追加します。

dependencyResolutionManagement {
    // ...
    versionCatalogs {
        create("moko") {
            from(files("./gradle/libs.versions.toml"))
        }
    }
}

rootのbuild.gradle.ktsに下記を追加します。

buildscript {
repositories {
        mavenLocal()
    }
    dependencies {
        classpath(moko.resourcesGradlePlugin)
    }
}

最後に sharedのbuild.gradle.ktsに下記を追加します。

plugins {
    // ...
    id("dev.icerock.mobile.multiplatform-resources")
}

sourceSets {
        val commonMain by getting {
            dependencies {
                // ...
                api(moko.resources)
                api(moko.resourcesCompose)
            }
         }
        // ...
}

multiplatformResources {
    multiplatformResourcesPackage = "com.myapplication.common"
}

moko-resourceを利用する場合、もしcocoapods内に次の表記があったら編集するか削除しておくのをお勧めします。

cocoapods {
    // extraSpecAttributes["resources"] = "['src/commonMain/resources/**', 'src/iosMain/resources/**']" // この記述がある場合は編集または削除
}

リソースを配置する

今回は画像を配置します。 shared/commonMain/resources の下にMR/images ディレクトリを作ります。

この中に配置した画像(ex.sample.png)をコード上で次のように呼び出すことができます。

Image(painter = painterResource(MR.images.sample)

画像のファイル名はiOSで画像を取得するために次のようなフォーマットにする必要があります。

  • [filename]@1x.png
  • [filename]@2x.png
  • [filename]@3x.png

(iOSをやっている方からしたら当たり前なのかも)

画像を表示

画像を表示します

Image(
            painter = painterResource(MR.images.sample),
            contentDescription = null
)

AndroidだとR.drawable.sampleと書くところですが、moko-resourceだとMR.images.sampleのように書きます。

料理の画像を一覧表示する

moko-リソースの使い方がわかったところでアプリを実装してみます。

モデル

import dev.icerock.moko.resources.ImageResource

data class Recipe(
    val resource: ImageResource,
    val title: String
)

リソースを取得するリポジトリ

リポジトリじゃなくてもいいんですが、慣れているのでリポジトリで実装しています。 APIとか立ててないし、Jsonファイルにもしてないのでハードコーディングしてますがここはご自由に。

class RecipeRepositoryImpl: RecipeRepository{
    override fun getRecipes(): List<Recipe> {
        return listOf(
            Recipe(
                title = "焼きそば",
                resource = MR.images.recipe1
            ),
            Recipe(
                title = "ハンバーガー",
                resource = MR.images.recipe2
            ),
            Recipe(
                title = "鶏チャーシュー",
                resource = MR.images.recipe3
            ),
            Recipe(
                title = "豚丼",
                resource = MR.images.recipe4
            ),
            Recipe(
                title = "山芋ソテー",
                resource = MR.images.recipe5
            ),
            Recipe(
                title = "団子",
                resource = MR.images.recipe6
            ),
            Recipe(
                title = "ラーメン",
                resource = MR.images.recipe7
            ),
            Recipe(
                title = "豚キムチ",
                resource = MR.images.recipe8
            ),
            Recipe(
                title = "唐揚げ",
                resource = MR.images.recipe9
            ),
            Recipe(
                title = "コーヒー",
                resource = MR.images.recipe10
            )
        )
    }
}

アイテムを表示するComposable

ちょっとイケてる感の演出のため画像の上にタイトルを被せて表示してみます。 dpとかspと書いてもiOSアプリで起動できるのでスゴイです。

@Composable
fun RecipeCell(
    modifier: Modifier = Modifier,
    recipe: Recipe
) {
    Box(modifier) {
        Image(
            painter = painterResource(recipe.resource),
            contentDescription = null,
            contentScale = ContentScale.Crop,
            modifier = Modifier.fillMaxWidth().aspectRatio(1f)
        )
        Column(modifier = Modifier
            .fillMaxWidth()
            .align(Alignment.BottomCenter)
            .background(
                Brush.verticalGradient(
                    colors = listOf(
                        Color.Transparent,
                        Color(0x80000000)
                    )
                )
            )
            .padding(vertical = 16.dp, horizontal = 8.dp)
        ) {
            Text(
                text = recipe.title,
                style = TextStyle(color = Color.White, fontSize = 18.sp, fontWeight = FontWeight.Bold)
            )
        }
    }
}

一覧表示するCompose

@Composable
fun GridList(recipes: List<Recipe>) {
    LazyVerticalGrid(columns = GridCells.Fixed(2)) {
        items(recipes.size) { index ->
            RecipeCell(recipe = recipes[index])
        }
    }
}

データを取得して一覧表示

@Composable
fun App() {
    MyTheme {
        val recipes = RecipeRepositoryImpl().getRecipes()
        Scaffold(
            topBar = {
                TopAppBar(
                    title = { Text(text = "Recipe Album") },
                )
            },
            content = {
                GridList(recipes)
            }
        )
    }
}

これで実行するとスクショのようになります。ほんとうにComposeでiOSアプリが作れます。 kotlinしか書いてないです。

所感

  • KMMとの相乗効果もあり、ComposeでiOSアプリが開発できるのはAndroidエンジニアとして作れるモノの広がりを感じる
  • 一方でリソースの扱いなど、プラットフォームの知識が必要になるシーンもあるので完全にAndroidのみの知識だけで完結するわけではない
  • アルファ版ではあるものの個人開発で使う選択肢の1つになりそう(てかかなりアリ)