【Xcode/SwiftUI】Creating a Dynamic List of Pokemon with SwiftUI and JSON Parsing

Introduction

Do you love Pokemon and want an easy way to browse through your favorite ones? Look no further than the SwiftUI Pokemon app! With a sleek user interface and simple navigation, this app is perfect for any Pokemon enthusiast.

Built using the Model-View-ViewModel (MVVM) design pattern and SwiftUI, the SwiftUI Pokemon app allows you to browse through a list of Pokemon, each with its own image, name, types, and description. The app’s data is powered by a local JSON file, which contains information on a select few Pokemon.

The app’s clean and intuitive design allows you to quickly browse through Pokemon and view their information, making it easy to find your favorites. Plus, the asynchronous image loading ensures that you can see all of the Pokemon images without any lag or slowdown.

In this blog post, we’ll take a closer look at the SwiftUI Pokemon app, including its structure, functionality, and how it utilizes MVVM and SwiftUI to create a seamless user experience. So whether you’re a die-hard Pokemon fan or just interested in learning more about SwiftUI and MVVM, keep reading to learn all about the SwiftUI Pokemon app!

Code

Local JSON File

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

Model

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

The “Model” in the SwiftUI Pokemon app refers to the Pokemon struct and the PokemonResponse struct. These structs serve as the data models for the app, which hold the necessary information for each Pokemon in the app.

The Pokemon struct contains the following properties:

  • id: an integer representing the unique identifier for the Pokemon
  • name: a string representing the name of the Pokemon
  • types: an array of strings representing the types of the Pokemon (e.g. “Fire”, “Water”)
  • description: a string representing a brief description of the Pokemon
  • imageUrl: a URL representing the location of the image for the Pokemon

The CodingKeys enum is used to map the property names in the JSON data to the corresponding property names in the Pokemon struct.

The PokemonResponse struct is used to represent the entire response from the JSON data, which contains an array of Pokemon structs.

Together, these structs provide a clear and organized way to store the Pokemon data for the app, making it easy to access and display the necessary information for each Pokemon in the app’s list view.

View

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

The “View” in the SwiftUI Pokemon app refers to the PokemonView and pokemonDataList functions. These elements are responsible for rendering the UI of the app and displaying the list of Pokemon.

The PokemonView is a SwiftUI view that contains a List view, which is populated with the Pokemon data fetched by the ViewModel. The List view takes in the array of Pokemon and generates a view for each Pokemon in the array using the pokemonDataList function.

The pokemonDataList function generates a view for a single Pokemon, which includes an AsyncImage view for the Pokemon’s image, and Text views for the Pokemon’s name, types, and description. The AsyncImage view loads the Pokemon’s image asynchronously, ensuring that the UI remains responsive while the images are being fetched.

Overall, the View layer is responsible for creating the user interface of the app and presenting the Pokemon data in a clear and easy-to-read format. The use of SwiftUI allows for a declarative and reactive approach to UI development, making it easier to build and maintain complex UIs.

ViewModel

import SwiftUI

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

The “ViewModel” in the SwiftUI Pokemon app refers to the PokemonViewModel class. The ViewModel is responsible for managing the data and logic of the app, acting as a middleman between the View and Model layers.

In this app, the PokemonViewModel contains a published property “pokemon” which stores an array of Pokemon structs. The ViewModel also contains a method “fetchPokemon” that uses URLSession to fetch Pokemon data from a JSON file bundled with the app.

When the fetchPokemon method is called, it retrieves the URL of the JSON file and uses URLSession to download the file contents. If the download is successful, the JSON data is decoded using the JSONDecoder class and the Pokemon array is updated with the new data.

The use of a ViewModel in this app allows for a separation of concerns between the View and Model layers. The View layer is able to focus solely on displaying the data, while the ViewModel handles all of the data fetching and processing. This separation of concerns makes the app more modular and easier to maintain, as changes to one layer do not affect the others.

Conclusion

The SwiftUI Pokemon app demonstrates the power and simplicity of the SwiftUI framework in creating a clean and responsive user interface. By utilizing the Model-View-ViewModel (MVVM) architecture, the app is able to separate the concerns of data management and user interface, making the app more modular and easier to maintain.

The Model layer defines the Pokemon struct and PokemonResponse struct, which represent the data model used in the app. The View layer is responsible for rendering the user interface of the app, using SwiftUI to generate a list of Pokemon and their respective information. Finally, the ViewModel layer is responsible for managing the data and logic of the app, communicating with the Model layer to fetch the data and update the UI accordingly.

Overall, the SwiftUI Pokemon app is a great example of how SwiftUI and MVVM can be used to create a simple yet powerful app with a clean and responsive user interface.