はじめに
こんにちは。奥江です。なかなかスラスラとコードが書けるようにならないです、、、
プログラマー向いてないんかなぁ笑
勉強がてらポケモン図鑑を作りたくなったのでポケモンAPIを叩いてポケモン図鑑を作成します。
では早速コードの解説を行います!!
ちなみにクリーンアーキテクチャを意識してコーディングしております。
全体像としては
APIRequest ↔︎ Repository ↔︎ Usecase ↔︎ ViewController
このような感じですね。
コードでだめなところ指摘して下さい
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で何回もリクエストしてしまっているのが良くない気がする。
解説
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年半くらいでこの程度です。もっと頑張らないとなぁ。
毎日勉強ですね。最近は英会話も始めました。常に学習して脳みそフル活用したい