geDemのアバターアイコン
geDem
 Blog
記事のカバー写真

Jetpack Compose(Kotlin)で、SwiftのKeyPath的なやつ使ってリスト表示したいなぁ〜

geDemのアバターアイコン
geDem
ペン

やりたいこと

SwiftにはKeyPathという機能があります。

「構造体のプロパティにアクセスできるパス」みたいなイメージです。

Viewの引数にKeyPathを与えると、型を明確に決めなくても値を取得できるので、例えば以下みたいなリストを表示するときとかに便利です。

リスト

ListView.swiftstruct ListView<ItemType: Identifiable>: View {
    // リストデータをジェネリクスで受け取る
    let listData: [ItemType]
    // KeyPathを受け取る
    let title: KeyPath<ItemType, String>
    let description: KeyPath<ItemType, String>
    
    var body: some View {
        VStack {
            ForEach(listData) { item in
                VStack(alignment: .leading, spacing: .zero) {
                    HStack {
                        // KeyPathを使って値にアクセス
                        Text(item[keyPath: title])
                        Text(item[keyPath: description])
                    }
                    Divider()
                }
            }
        }
        .frame(width: 200, alignment: .leading)
    }
}

struct SomeType1: Identifiable {
    var id: UUID = UUID()
    let title: String
    let description: String
}

struct SomeType2: Identifiable {
    var id: UUID = UUID()
    let name: String
    let bio: String
}

#Preview {
    VStack(spacing: 30) {
        ListView(
            listData: [
                SomeType1(title: "朝ごはん", description: "食べる"),
                SomeType1(title: "自転車", description: "こぐ"),
                SomeType1(title: "パソコン", description: "叩く")
            ],
            // KeyPath指定
            title: \SomeType1.title,
            description: \SomeType1.description
        )

        ListView(
            listData: [
                SomeType2(name: "たろう", bio: "釣りが好き"),
                SomeType2(name: "じろう", bio: "ゲームが好き"),
                SomeType2(name: "さぶろう", bio: "なんでも好き")
            ],
            title: \SomeType2.name,
            description: \SomeType2.bio
        )
    }
}

こんな感じのがJetpack Composeで実装できたら便利なのになぁと思って試行錯誤しました。

Jetpack Composeでの実装方法

要は、インスタンスから値にアクセスできれば良いので、クロージャで実装できます。

ListView.kt@Composable
fun <ListItem> ListView(
    listData: List<ListItem>,
    titleSelector: (ListItem) -> String,
    descriptionSelector: (ListItem) -> String
) {
    Column(Modifier.width(200.dp)) {
        listData.map { item ->
            Column {
                Row {
                    // クロージャにインスタンスを渡す
                    Text(text = titleSelector(item))
                    Text(text = descriptionSelector(item))
                }
                Divider()
            }
        }
    }
}

data class SomeType1(
    val title: String,
    val description: String
)

data class SomeType2(
    val name: String,
    val bio: String
)

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    PlaygroundTheme {
        Column(
            Modifier.fillMaxSize(),
            verticalArrangement = Arrangement.spacedBy(
                space = 30.dp,
                alignment = Alignment.CenterVertically
            ),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            ListView(
                listData = listOf(
                    SomeType1(title = "朝ごはん", description = "食べる"),
                    SomeType1(title = "自転車", description = "こぐ"),
                    SomeType1(title = "パソコン", description = "叩く")
                ),
                // インスタンスの値にアクセス
                titleSelector = { it.title },
                descriptionSelector = { it.description }
            )

            ListView(
                listData = listOf(
                    SomeType2(name = "たろう", bio = "釣りが好き"),
                    SomeType2(name = "じろう", bio = "ゲームが好き"),
                    SomeType2(name = "さぶろう", bio = "なんでも好き")
                ),
                titleSelector = { it.name },
                descriptionSelector = { it.bio }
            )
        }
    }
}

リスト

🙂‍↕️最後まで読んでいただきありがとうございます🙂‍↕️