2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ポケモン図鑑アプリを作ってみた

Last updated at Posted at 2023-07-08

はじめに

こんにちは。奥江です。なかなかスラスラとコードが書けるようにならないです、、、
プログラマー向いてないんかなぁ笑

勉強がてらポケモン図鑑を作りたくなったのでポケモンAPIを叩いてポケモン図鑑を作成します。
では早速コードの解説を行います!!

ちなみにクリーンアーキテクチャを意識してコーディングしております。
全体像としては

APIRequest ↔︎ Repository ↔︎ Usecase ↔︎ ViewController 
このような感じですね。

コードでだめなところ指摘して下さい:v:

APIRequestクラス

class ApiRequest {
    
    static let shard = ApiRequest()
    static let baseUrl = "https://pokeapi.co/api/v2/pokemon/"
    
    private init() {}
    
    let ids = 1...100
    
    func request(handler: @escaping (Result<JSON, ApiError>) -> Void) {
        guard let url = URL(string: Self.baseUrl) else {
            return
        }
        ids.forEach { id in
            let url = url.appendingPathComponent("\(id)", isDirectory: false)
            AF.request(URLRequest(url: url)).responseData { responce in
                switch responce.result {
                case .success(let responceData):
                    if let statusCode = responce.response?.statusCode {
                        switch statusCode {
                        case 200:
                            handler(.success(JSON(responceData)))
                        case 400:
                            handler(.failure(.badRequest))
                        case 401:
                            handler(.failure(.unauthorized))
                        case 500:
                            handler(.failure(.internalServerError))
                        default:
                            handler(.failure(.internalServerError))
                        }
                    }
                case .failure:
                    handler(.failure(.internalServerError))
                }
            }
        }
    }
}

enum ApiError: Error {
    case noNetworkConnection
    case netWorkTimeOut
    case unauthorized
    case badRequest
    case internalServerError
    case unknown
    case illegalResponse(case: EntitiyCreationError)
}

struct EntitiyCreationError: Error {
    let responseJson: String
}

今回はalamofireを使ってリクエストをしております。
おそらくこの書き方は良くないのかなぁと思いつつ書いています。
というのもforで何回もリクエストしてしまっているのが良くない気がする。:frowning2:

解説
ApiRequestクラスはシングルトンパターンにしました。というのもインスタンスが複数個必要ないので
常に1つでいいかなと思いました。(シングルトンパターンもっと勉強します)

baseUrlは変わることがないのとインスタンスに影響を受けたくないのでstaticにしております。

requestクラスですが、引数にhandlerを実装しました。
aysnc/awitで書けてしまうのですが今回は復習がてらクロージャにしました。
いわゆるコールバックですね。私はこれを理解するのに1年もかかりました笑、、
皆さんはすぐ理解できましたか?クロージャ難しいですよね、、
APIから返却されたdata型をJsonに変換してクロージャの引数に詰めて返しております。

Repository

protocol PokemonRepository {
    // ポケモンの情報を取得する
    func pokemonInfo(handler: @escaping (Result<Pokemon, ApiError>) -> Void)
}

class PokemonRepositoryImpl: PokemonRepository {
    func pokemonInfo(handler: @escaping (Result<Pokemon, ApiError>) -> Void) {
        ApiRequest.shard.request { result in
            switch result {
            case .success(let json):
                do {
                    handler(.success( try Pokemon(json: json)))
                } catch let error as EntitiyCreationError {
                    handler(.failure(.illegalResponse(case: error)))
                } catch {
                    fatalError(error.localizedDescription)
                }
            case .failure(let error):
                handler(.failure(error))
            }
        }
    }
}

RepositoryレイヤーでAPIで取得した結果をPokemonモデルにパースしております。
先ほどのApiRequestクラスの結果を下記でresultと受け取っております。
do-catchでエラーハンドリングをしていますね!
do-catchはdoのスコープ内でエラーが発生する可能性のある処理を記述します。
エラーが発生するとcatchしてエラー処理ですね。今回はPokemonのイニシャライザで初期化失敗する可能性が
あったので catch let error as EntitiyCreationErrorにしました。

あっ、ここでもクロージャの登場です、、笑
今回も引数にResult型の引数にとるクロージャがありますね。
これもコールバックです!

ApiRequest.shard.request { result in
            switch result {
            case .success(let json):
                do {
                    handler(.success( try Pokemon(json: json)))
                } catch let error as EntitiyCreationError {
                    handler(.failure(.illegalResponse(case: error)))
                } catch {
                    fatalError(error.localizedDescription)
                }
            case .failure(let error):
                handler(.failure(error))
            }
        }

Usecase

特に今回は何もしていないですね、、

class PokemonUsecase {
    
    let repository = PokemonRepositoryImpl()
    
    func pokemonInfo(handler: @escaping (Result<Pokemon, ApiError>) -> Void) {
        repository.pokemonInfo(handler: handler)
    }
}

Pokemonモデル

struct Pokemon {
    
    enum Attribute: String {
        case normal
        case water
        case fire
        case grass
        case electric
        case ice
        case psychic
        case fighting
        case poison
        case ground
        case flying
        case bug
        case rock
        
        var title: String {
            switch self {
            case .normal:
                return "ノーマル"
            case .water:
                return "みず"
            case .fire:
                return "ほのお"
            case .grass:
                return "くさ"
            case .electric:
                return "でんき"
            case .ice:
                return "こおり"
            case .psychic:
                return "エスパー"
            case .fighting:
                return "かくとう"
            case .poison:
                return "どく"
            case .ground:
                return "じめん"
            case .flying:
                return "ひこう"
            case .bug:
                return "むし"
            case .rock:
                return "いわ"
            }
        }
        
        var color: UIColor {
            switch self {
            case .normal:
                return UIColor.gray
            case .water:
                return UIColor.blue
            case .fire:
                return UIColor.orange
            case .grass:
                return UIColor.green
            case .electric:
                return UIColor.yellow
            case .ice:
                return UIColor.cyan
            case .psychic:
                return UIColor.magenta
            case .fighting:
                return UIColor.red
            case .poison:
                return UIColor.purple
            case .ground:
                return UIColor.brown
            case .flying:
                return UIColor.cyan
            case .bug:
                return UIColor.green
            case .rock:
                return UIColor.brown
            }
        }
    }
    
    // ポケモンID
    let id: Int
    // ポケモンの名前
    let name: String
    // ポケモンのimageUrl
    let imageUrl: String
    // ポケモンのタイプ
    let attributes: [Attribute]
    
}

extension Pokemon {
    init(json: JSON) throws {
        guard let id = json["id"].int,
              let name = json["name"].string,
              let imageUrl = json["sprites"]["front_default"].string,
              let attributes = json["types"].array else {
            throw EntitiyCreationError(responseJson: json.debugDescription)
        }
        self.id = id
        self.name = name
        self.imageUrl = imageUrl
        self.attributes = attributes.compactMap {
            Pokemon.Attribute(rawValue: $0["type"]["name"].stringValue)
        }
    }
}

swiftyJsonを使って簡単にJsonからデータを取得しております。
今回はポケモンのIDと名前、タイプと画像Urlを取得しております。
タイプはenum Attributeを作成しております。
enumの初期化は

self.attributes = attributes.compactMap {
            Pokemon.Attribute(rawValue: $0["type"]["name"].stringValue)
        }

ここで行っていますね。

後はguard letで値が取得できなかった際にエラーが投げております。
なのでこのイニシャライザを呼び出す際はtry Pokemon〜のように記述しなければいけません。

ViewController

class ViewController: UIViewController {

    @IBOutlet weak var tableView: UITableView!
    
    let usecase = PokemonUsecase()
    
    var pokemonList: [Pokemon] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        prepareSetting()
    }
    
    private func prepareSetting() {
        tableView.delegate = self
        tableView.dataSource = self
        tableView.register(UINib(nibName: String(describing: PokemonTableViewCell.self), bundle: nil),
                           forCellReuseIdentifier: String(describing: PokemonTableViewCell.self))
        tableView.backgroundColor = .white
    }
    
    @IBAction func acquisitionButtonDidTap(_ sender: Any) {
        usecase.pokemonInfo { [weak self] result in
            switch result {
            case .success(let pokemon):
                self?.pokemonList.append(pokemon)
                self?.tableView.reloadData()
            case .failure:
                self?.showError(title: "ポケモンセンターで回復して下さい",
                                message: "",
                                actioins: [UIAlertAction(title: "OK",
                                                         style: .default)])
            }
        }
    }
}

extension ViewController: UITableViewDelegate {
    
}

extension ViewController: UITableViewDataSource {
    func numberOfSections(in tableView: UITableView) -> Int {
        1
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        pokemonList.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PokemonTableViewCell.self), for: indexPath) as! PokemonTableViewCell
        cell.pokemon = pokemonList[indexPath.row]
        return cell
    }
}

ついにViewControllerですね!!
やっとPokemonのデータがViewControllerに渡ってきました。

まぁやっていることは、tableViewをセッティングしてボタンがタップされるとAPIをリクエストしに行って
返却された値をtableVeiwで表示している感じですね。cellForRowAtでcellにポケモンのデータを渡してcellで表示ロジックを書いております。

TableViewCell

class PokemonTableViewCell: UITableViewCell {
    
    @IBOutlet weak var pokemonImageView: UIImageView!
    @IBOutlet weak var idLabel: UILabel!
    @IBOutlet weak var nameLabel: UILabel!
    @IBOutlet weak var attributeLabel1: UILabel!
    @IBOutlet weak var attributeLabel2: UILabel!
    
    private lazy var attributeLabels = [attributeLabel1,
                                        attributeLabel2]
    
    let imageDownloader = PokemonImageDownloader.shard
    
    var pokemon: Pokemon? {
        didSet {
            guard let pokemon else {
                return
            }
            configure(pokemon: pokemon)
        }
    }
    
    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
    }

    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)

        // Configure the view for the selected state
    }
    
    private func configure(pokemon: Pokemon) {
        guard pokemon.id == self.pokemon?.id else {
            return
        }
        pokemonImageView.image = nil
        idLabel.text = String(pokemon.id)
        nameLabel.text = pokemon.name
        attributeLabel2.isHidden = pokemon.attributes.count == 1
        pokemon.attributes.enumerated().forEach { offset, element in
            attributeLabels[offset]?.text = element.title
            attributeLabels[offset]?.textColor = element.color
        }
        imageDownloader.downloadImage(imageUrl: pokemon.imageUrl) { [weak self] result in
            switch result {
            case .success(let image):
                self?.pokemonImageView.image = image
            case .failure:
                self?.pokemonImageView.image = nil
            }
        }
    }
}

ここでは受けとったデータを表示しているだけですね。
少し解説すると、

        pokemon.attributes.enumerated().forEach { offset, element in
            attributeLabels[offset]?.text = element.title
            attributeLabels[offset]?.textColor = element.color
        }

enumrated()でoffsetとelement、要するにindexと要素が取得できるようになります。

PokemonImageDownloader

class PokemonImageDownloader {
    
    static let shard = PokemonImageDownloader()
    private init() {}
    
    let downloader = ImageDownloader()
    
    func downloadImage(imageUrl: String, completionHandler: @escaping (Result<UIImage, Error>) -> Void) {
        guard let url = URL(string: imageUrl) else {
            return
        }
        
        downloader.download(URLRequest(url: url), completion: { responce in
            switch responce.result {
            case .success(let image):
                completionHandler(.success(image))
            case .failure(let error):
                completionHandler(.failure(error))
            }
        })
    }
}

解説します!
画像urlの文字列を引数に受けとって、そこから画像をalamofireImageを使って画像を生成しております。
ここでもクロージャが登場していますね。

作ってみた感想

如何でしたでしょうか?
書き方等こうした方がいいよ的なことがあればどしどしお願いします🙇

ちなみに私は東京で働いております。28歳で転職してエンジニアになったのでかなり遅いと思います。
現在29歳でエンジニア歴としては1年半くらいでこの程度です。もっと頑張らないとなぁ。
毎日勉強ですね。最近は英会話も始めました。常に学習して脳みそフル活用したい

2
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?