【Xcode/SwiftUI】SwiftUI × JSONでポケモンリストを作る

準備

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"
        }
    ]
}

MVVMファイルの作成

以下3つのファイルに分けておく

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

コーディング

そのままコピペでOK

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"
    }
}
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()
    }
}
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()
    }
}

完成形