[SwiftUI] LazyVGridを使ってブックアプリの画面を再現してみた

## はじめに
初投稿です。
SwiftUIの勉強中です。
LazyVGridはさまざまな画面で使えそうだったので、
Apple公式のブックアプリのUIから近いものを選び、学んでみました。

## 作成した画面
- 戻るボタンと下タブは未対応です
- 漫画の情報は固定データです
  - ChatGPTに調べてもらいました
  - サムネイルには自宅のワンちゃんの写真を使っています


|参考にした画面|作ってみた結果|
|---|---|
|<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4097135/166946c7-7b98-41be-9779-a698877ad9cb.png">|<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4097135/71090cfb-ae8e-4b1c-9b6b-74730c93b964.png">|

## コード
3つのファイルに分けて作成しました。

- Manga.swift
  - 漫画の情報用のEntity
- CellView.swift
  - LazyVGridで使用するパーツを作成している
- ContentView.swift
  -  LazyVGridを使っている大元の画面

### Manga.swift
```swift:Manga.swift
struct Manga {
    /// 作品名
    let title: String
    /// 作者名
    let author: String
    /// 金額
    let price: Int
    /// サムネイル（サンプルなので固定値）
    let thumbnailString: String
}

/// サンプルデータ
let mangaList: [Manga] = [
    Manga(title: "SPY×FAMILY 15", author: "遠藤達哉", price: 880, thumbnailString: "toypoodle"),
    Manga(title: "キングダム 75", author: "原泰久", price: 590, thumbnailString: "toypoodle"),
    Manga(title: "ブルーロック(33)", author: "金城宗幸 & ノ村優介", price: 880, thumbnailString: "toypoodle"),
    Manga(title: "終末のワルキューレ 24巻", author: "アジチカ, 梅村真也 & フクイタクミ", price: 760, thumbnailString: "toypoodle"),
    Manga(title: "風都探偵(18)", author: "石ノ森章太郎, 三条陸 & 佐藤まさき", price: 600, thumbnailString: "toypoodle"),
    Manga(title: "太陽と月の鋼(10)", author: "松浦だるま", price: 760, thumbnailString: "toypoodle"),
    Manga(title: "異世界でスローライフを(願望) 9", author: "長頼, シゲ & オウカ", price: 750, thumbnailString: "toypoodle"),
    Manga(title: "ヤングジャンプ 2025 No.17", author: "ヤングジャンプ編集部", price: 760, thumbnailString: "toypoodle"),
    Manga(title: "追放されるたびにスキルを手に入れた俺が、100の異世界で2周目無双 4", author: "仁森島司, 日之浦拓 & GreeN", price: 400, thumbnailString: "toypoodle"),
    Manga(title: "創世のタイガ 13巻", author: "森恒二", price: 770, thumbnailString: "toypoodle"),
    Manga(title: "現実主義勇者の王国再建記XIII", author: "上田悟司, どぜう丸 & 冬ゆき", price: 760, thumbnailString: "toypoodle"),
    Manga(title: "片田舎のおっさん、剣聖になる外伝 竜双剣の軌跡 1巻", author: "上田悟司, どぜう丸 & 冬ゆき", price: 760, thumbnailString: "toypoodle"),
    Manga(title: "D級冒険者の俺、なぜか勇者パーティーに勧誘されたあげく、王女につきまとわれてる 6", author: "舘津テト, 白青虎猫 & りいちゅ", price: 760, thumbnailString: "toypoodle"),
    Manga(title: "異世界のすみっこで快適ものづくり生活2", author: "西山アラタ & 長田信織", price: 760, thumbnailString: "toypoodle"),
    Manga(title: "午後の光線", author: "南寝", price: 880, thumbnailString: "toypoodle"),
    Manga(title: "うしろの席のぎゃるに好かれてしまった。(2)", author: "栖夜まろみ, 陸奥こはる & 緋月ひぐれ", price: 880, thumbnailString: "toypoodle"),
    Manga(title: "ギャラリーフェイク(39)", author: "細野不二彦", price: 880, thumbnailString: "toypoodle"),
    Manga(title: "航宙軍士官、冒険者になる8", author: "たくま朋正, 伊藤暖彦 & himesuz", price: 760, thumbnailString: "toypoodle"),
    Manga(title: "アルスラーン戦記(22)", author: "荒川弘 & 田中芳樹", price: 590, thumbnailString: "toypoodle"),
    Manga(title: "かまって新卒ちゃんが毎回誘ってくる4", author: "不明", price: 600, thumbnailString: "toypoodle"),
    Manga(title: "ハコヅメ～交番女子の逆襲～(1)", author: "泰三子", price: 600, thumbnailString: "toypoodle"),
    Manga(title: "転生したらスライムだった件(1)", author: "川上泰樹, 伏瀬 & みっつばー", price: 600, thumbnailString: "toypoodle"),
    Manga(title: "ルックバック", author: "藤本タツキ", price: 600, thumbnailString: "toypoodle"),
    Manga(title: "聲の形(1)", author: "大今良時", price: 600, thumbnailString: "toypoodle"),
    Manga(title: "鬼滅の刃 1", author: "吾峠呼世晴", price: 600, thumbnailString: "toypoodle"),
    Manga(title: "ダイヤモンドの功罪 1", author: "平井大橋", price: 600, thumbnailString: "toypoodle"),
    Manga(title: "神田ごくら町職人ばなし 〈一〉", author: "坂上暁仁", price: 600, thumbnailString: "toypoodle"),
    Manga(title: "サチ録～サチの黙示録～ 1", author: "茶んた", price: 600, thumbnailString: "toypoodle"),
    Manga(title: "環と周", author: "よしながふみ", price: 600, thumbnailString: "toypoodle"),
    Manga(title: "恋せよまやかし天使ども(1)", author: "卯月ココ", price: 600, thumbnailString: "toypoodle")
]
```

### CellView.swift
表示内容は以下の4つの要素で構成されています
- サムネイル：Image
- 作品名：Text
- 作者名：Text
- 金額：Text

preview
<img width="100" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4097135/bcc50a64-c4e0-49b3-8a86-2eb788040b56.png">

```swift:CellView.swift
import SwiftUI

struct CellView: View {
    let manga: Manga
    
    var body: some View {
        VStack(spacing: 12) {
            // サムネイル
            Image(decorative: manga.thumbnailString)
                .resizable()
                .aspectRatio(3/4, contentMode: .fit)
                .cornerRadius(2)
                .shadow(
                    color: .black.opacity(0.2),
                    radius: 6,
                    x: 2,
                    y: 10
                )
            VStack(alignment: .leading, spacing: 0) {
                // 作品名
                Text(manga.title)
                    .lineLimit(1)
                    .font(.system(size: 14))
                    .foregroundColor(.black)
                    .fontWeight(.bold)
                // 作者名
                Text(manga.author)
                    .lineLimit(1)
                    .font(.system(size: 12))
                    .foregroundColor(.gray)
                // 金額
                Text("¥\(String(manga.price))")
                    .lineLimit(1)
                    .font(.system(size: 12))
                    .fontWeight(.bold)
                    .foregroundColor(.black)
                    .padding(.horizontal, 6)
                    .padding(.vertical, 2)
                    .background(
                        // 枠線
                        RoundedRectangle(cornerRadius: 12)
                            .stroke(.black, lineWidth: 1)
                    )
                    .padding(.top, 4)
            }
            .frame(maxWidth: .infinity, alignment: .leading)
            .padding(.trailing, 20)
        }
    }
}

#Preview {
    CellView(manga: Manga(title: "作品名", author: "作者名", price: 100, thumbnailString: "toypoodle"))
}

```
### ContentView.swift
- columnsに列のレイアウトを設定
- LazyVGridで、CellViewを縦2列で表示するよう設定
- ScrollViewで内包する事で、スクロールができている


```swift:ContentView.swift
import SwiftUI

struct ContentView: View {
    // レイアウトを指定
    // 均等な大きさ、2列、間のスペースを20px
    let columns = [
        GridItem(.flexible(), spacing: 20),
        GridItem(.flexible())
    ]
    
    var body: some View {
        NavigationView {
            ScrollView {
                // columns：列のレイアウト
                // spacing：行同士のスペース
                LazyVGrid(columns: columns, spacing: 20) {
                    // mangaListの数だけループ
                    ForEach(mangaList.indices, id: \.self) { index in
                        // CellViewにmangaListの情報をindex順に適用
                        let manga = mangaList[index]
                        CellView(manga: manga)
                    }
                }
                .padding()
            }
            .navigationTitle("おすすめの新刊")
            .navigationBarTitleDisplayMode(.inline)
        }
    }
}

#Preview {
    ContentView()
}
```

## やってみた感想
UIKitと比べて、レイアウトの設定がとても簡単だと感じました。
こんなに少ないコードで画面が作れるとは…！
`.padding`の使い方は、工夫次第でもっと簡潔にできるかもしれませんね。
（勉強中だから追々慣れるはずだと信じる）
次はLazyHGridを触ってみようと思います。

間違っている点や、もっとこうした方がいいよ！
という意見もあれば、よろしくお願いします:open_hands: