【Xcode/SwiftUI】SwiftUI × JSONのポケモンリスト模写してみた

こんにちは！今日は多くの人がされているポケモンのリスト作成をやってみました。
JSONファイルを作成しそのJSONファイルをもとにポケモンのリストを表示するというアプリになります。
簡単でまたとても楽しかったのでぜひ作られてみてください。

アプリの構成はMVVMアーキテクチャで作成します。
まずは完成系がこちら！！

<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2138087/caf92765-378b-ac36-76a9-6f5bb40d98cc.png
">

なんかいい感じですね。
では作り方です。

まずはJSONファイルを作成します。私はJSON Online Editorを使ってファイルを作成しました。
https://jsoneditoronline.org/

pokemon.json
```json
{
    "pokemon": [
        {
            "id": 1,
            "name": "Bulbasaur",
            "types": ["Grass", "Poison"],
            "description": "A strange seed was planted on its back at birth. The plant sprouts and grows with this Pokémon.",
            "image_url": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/1.png"
        },
        {
            "id": 2,
            "name": "Charmander",
            "types": ["Fire"],
            "description": "Obviously prefers hot places. When it rains, steam is said to spout from the tip of its tail.",
            "image_url": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/4.png"
        },
        {
            "id": 3,
            "name": "Squirtle",
            "types": ["Water"],
            "description": "After birth, its back swells and hardens into a shell. Powerfully sprays foam from its mouth.",
            "image_url": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/7.png"
        },
        {
            "id": 4,
            "name": "Pikachu",
            "types": ["Electric"],
            "description": "When several of these Pokémon gather, their electricity could build and cause lightning storms.",
            "image_url": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/25.png"
        },
        {
            "id": 5,
            "name": "Eevee",
            "types": ["Normal"],
            "description": "Its genetic code is irregular. It may mutate if it is exposed to radiation from element Stones.",
            "image_url": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/133.png"
        },
        {
            "id": 6,
            "name": "Mew",
            "types": ["Psychic"],
            "description": "So rare that it is still said to be a mirage by many experts. Only a few people have seen it worldwide.",
            "image_url": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/151.png"
        },
        {
            "id": 7,
            "name": "Gengar",
            "types": ["Ghost", "Poison"],
            "description": "Under a full moon, this Pokémon likes to mimic the shadows of people and laugh at their fright.",
            "image_url": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/94.png"
        },
        {
            "id": 8,
            "name": "Dragonite",
            "types": ["Dragon", "Flying"],
            "description": "This Pokémon is so strong, it can easily hold aloft a child while flying.",
            "image_url": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/149.png"
        },
        {
            "id": 9,
            "name": "Mewtwo",
            "types": ["Psychic"],
            "description": "It was created by a scientist after years of horrific gene-splicing and DNA-engineering experiments.",
            "image_url": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/150.png"
        },
        {
            "id": 10,
            "name": "Sylveon",
            "types": ["Fairy"],
            "description": "It sends a soothing",
            "image_url": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/700.png"
        }
    ]
}
```

この作成したファイルをXcodeに入れます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2138087/1e8a5ecc-e69d-41e4-c8f2-77072302f3ce.png)


### ファイルの作成
次にファイルを３つ作成します

Pokemon.swift [Model]
PokemonView.swift [View]
PokemonViewModel.swift [ViewModel]

そしてContentViewを消してAppファイルのWindowGroupをPokemonViewに変更しましょう
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2138087/72fc68a9-0364-1739-bc8b-199e164567e2.png)


## Model
Pokemon.swift
```swift
import Foundation

struct PokemonResponse: Codable {
    let pokemon: [Pokemon]
}

struct Pokemon: Codable, Hashable {
    let id: Int
    let name: String
    let types: [String]
    let description: String
    let imageUrl: URL

    enum CodingKeys: String, CodingKey {
        case id
        case name
        case types
        case description
        case imageUrl = "image_url"
    }
}
```


## ViewModel

```swift
import Foundation

class PokemonViewModel: ObservableObject {
    @Published var pokemon: [Pokemon] = []

    func fetchPokemon() {
        guard let url = Bundle.main.url(forResource: "pokemon", withExtension: "json") else {
            print("File not Found")
            return
        }
        let task = URLSession.shared.dataTask(with: url) { data, _, error in
            if let data = data {
                if let decodedData = try? JSONDecoder().decode(PokemonResponse.self, from: data) {
                    DispatchQueue.main.async {
                        self.pokemon = decodedData.pokemon
                    }
                }
            }
        }
        task.resume()
    }
}
```


## View

```swift
import SwiftUI

struct PokemonView: View {
    @ObservedObject private var viewModel = PokemonViewModel()

    var body: some View {
        List(viewModel.pokemon, id: \.self) { pokemon in
            pokemonDataList(pokemon)
        }
        .onAppear {
            viewModel.fetchPokemon()
        }
    }
}

@ViewBuilder
private func pokemonDataList(_ pokemon: Pokemon) -> some View {
    HStack {
        ZStack {
            Circle()
                .fill(Color.gray.opacity(0.4))
                .frame(width: 80, height: 80)
            AsyncImage(url: pokemon.imageUrl) { phase in
                switch phase {
                case .empty:
                    ProgressView()
                case .success(let image):
                    image
                        .resizable()
                        .frame(width: 80, height: 80)
                case .failure:
                    Text("Failed to load image")
                @unknown default:
                    Text("Unknown error")
                }
            }
        }
        VStack(alignment: .leading) {
            HStack {
                Text(pokemon.name)
                    .font(.system(size: 24, weight: .light))
                Spacer()
                Text(pokemon.types.joined(separator: ", "))
                    .font(.system(size: 16, weight: .light))
                    .foregroundColor(.black.opacity(0.8))
            }
            Spacer().frame(height: 4)
            Text(pokemon.description)
                .foregroundColor(.gray.opacity(0.8))
                .font(.system(size: 12, weight: .light))
        }
    }
}

struct PokemonView_Previews: PreviewProvider {
    static var previews: some View {
        PokemonView()
    }
}
```

これでビルドするとアプリが完成するはずです。

# 完成形

<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2138087/3fd5ea6d-2fd1-37c5-a0ab-61a8ccaf764c.png
">