Help us understand the problem. What is going on with this article?

PokéAPIを利用してMVP+CleanArchitectureのiOSアプリを作ったので解説する

はじめに

最近、勉強会などで、iOSの業務未経験の人たちと話している時に、これからiOSエンジニアとして仕事を得るためにどういったことを学べばいいのか、業務で実際にどういうことを意識して設計やツールを駆使しているかということをよく聞かれるので、それについて説明することができたらなと感じていました。

実際に業務で作っているコードを公開することはできないので、業務で作っているコードにかなり近い形のサンプルのアプリを作成、公開したので、それを参考にしながら私がどういったことを考えてiOSの業務をこなしているかについてを解説していきます。

注意:これはあくまで私が個人的に思う業務における考え方であって、正解というわけではありません。一つの意見として捉えてもらえると幸いです。

最も重要なことは何か

私は、iOSに限らずアプリケーション開発の業務を遂行する上で、最も重要なことは、アプリケーションのリリースサイクルを短くして、ユーザーに最速で最大の価値を提供し続けることだと考えています。

それを達成するためには、以下の様なことを意識するのが効果的と考えて、業務で実践しています。

  • 自動化をする
  • ミスを減らす
  • 開発におけるストレスを減らす

今回はそういったことを意識して、業務で実際に使っている、ミスが起こりにくく、素早い変更や改修、機能追加を容易にするためのノウハウを実践的に詰め込んだアプリを作成しました。

Pokedex

そのノウハウを詰め込んだサンプルのアプリですが、普段業務で考えていることを解説するという、少し退屈なテーマを目的としたものである以上、せめてアプリのテーマだけでも面白いものにしたいと思い、今回はPokéAPIを利用してみました。

Pokedex

ポケモンの一覧と詳細を見ることができる簡単なポケモン図鑑のアプリです。

セットアップ編

では、早速コードを見て解説と言いたいところですが、このアプリを起動するまでには、少しセットアップの作業が必要なので、それをまずはしていきましょう。

Makefile

Makefileに従ってセットアップを行えば、ツールやライブラリのインストールなどの面倒な作業をかなり簡単にできるのでオススメです。

実際の業務では、新たにチームメンバーが参加した時などに役立つでしょう。

README.mdに書いてあるとおり、ターミナルでプロジェクトのルートを開き、make bootstrapmake projectのコマンドを実行します。

今回インストールしたツールはiOS開発を行う上で、効率を上げてくれるので詳細を説明するべきなのですが、全部を細かく紹介すると、かなり長くなってしまうので、それぞれについてさらっと紹介して、参考になる記事を貼り付けておきます。

Mint

Swift製のコマンドラインツールを管理することができるツールです。

今回はこれを利用して、XcodeGenなどの開発する際に便利なツールを管理します。

Mint で Swift 製のコマンドラインツールを管理する

Swift製コマンドラインツールのパッケージ管理ツール「Mint」のセットアップ&操作方法

Carthage

言わずとしれたライブラリ管理ツールです。

CocoaPodsに比べて、ライブラリのインストール時間が長いですが、その分コンパイル時間が短縮できるので、オススメです。

Carthageを使ってビルド時間を短縮しよう

XcodeGen

チームで開発しているとよくあるのですが、ファイル追加による.xcodeprojのコンフリクトに悩まされます。

ターミナルでxcodegenのコマンドを実行すれば、project.ymlファイルに記載されている内容から.xcodeprojを生成してくれるので、コンフリクトのストレスから解放されます。

Pokedexは一人で開発したので、コンフリクトの問題は元々起こりにくい環境ではあるのですが、マルチモジュールで開発しているため、その辺りの管理もやりやすいことから導入するメリットがあるなと思ってます。

XcodeGenによる新時代のiOSプロジェクト管理

SwiftGen

画像や色などのリソースを取得するためのファイルを自動生成してくれるツールです。

画像の呼び出しなどをする際の文字列の指定をtypoしてしまい取得できないといった、ミスを無くすことができます。

SwiftGenを導入して無駄なビルド負担を低減する

R.swiftとSwiftGenの導入方法とどちらを採用した方がいいのか

SwiftLint

.swiftlint.ymlファイルに記載されたコードのルールに応じて、ビルド時に警告を
出したりコンパイルエラーにさせることができるツールです。

コードにルールを設けることで書き方が統一されるので、コードの読みやすさが向上します。

簡単なルールなら、自動でコードの修正をしてくれる機能もあるので、そういった点でも入れておくとかなり楽になります。

Swiftの静的解析ツール「SwiftLint」のセットアップ方法

Rule Directory Reference

設計とマルチモジュール編

セットアップができたところで、Pokedexのアプリの全体の設計についてを先に見ていきましょう。

Pokedexでは、アーキテクチャにMVP + CleanArchitectureを採用しています。
ef5fc34c-7f99-4689-8f98-bcee1ff62735

設計(アーキテクチャ)について

業務での開発において私が最も重要視しているのはこれと言っても過言ではありません。

設計がしっかりしたソースコードは責務分けがしっかりされているので、特定の改修を加える際に楽になります。
その上、テストを書きやすくなるので、特定の改修を行った際にデグレが起きてないかのチェックを簡単にできるというメリットもあります。

業務であれば、新規開発する期間よりもアプリをリリースしてから運用して改修する期間の方が長いことがほとんどなので、改修をより早く容易にできる様にアプリの設計を採用すべきと考えています。

例えば、PokedexでAPI通信する際のアクセスする先のURLを変更したいという要望があった場合に、DataStoreのモジュールにある、アクセス先のURLを指定している部分を変更すればいいということが何となくつかめるので、すぐに改修でき、テストも書いてあれば問題がないかどうかもすぐに分かり、安心してアップデートすることができます。

もし設計が何もない状態で全てのロジックがUIViewController上に書かれたソースコードだと、まずはそのUIViewControllerのクラスを開き、そこから特定のソースコードを探して改修することになります。
それが何の責務わけもされていない、数千行のUIViewControllerだった場合、簡単な変更をするだけでも他の部分に影響がないか調査するだけでも一苦労です。

こういう状況で、素早いリリースサイクルをこなすということは、非常に難しくなるでしょう。

今回私はMVP + CleanArchitectureを採用しましたが、それでないとダメというわけではありません。

MVC、MVVM、VIPERなどの数々のアーキテクチャがありますが、これを使えば全部において最強!みたいなのは存在せず、リリースサイクルを短くするのに、最も都合がいい物を都度選択すればいいと私は考えています。

設計はあくまで目的を実現するための手段に過ぎないので、作りたいアプリのサイズ感や性質などに応じて適切なものを選択できる様になるといいでしょう。

アーキテクチャに関しては様々な記事が出ているので、もし詳細に知りたい場合は以下の記事を参考にするのをお勧めします。

現場で選ばれているiOSアーキテクチャ

まだMVC,MVP,MVVMで消耗してるの? iOS Clean Architectureについて

マルチモジュール化

最大の特徴はマルチモジュールで作成されているということです。

スクリーンショット 2020-04-05 7 17 15

アプリのターゲットに加えて、DataStoreDomainPresentationの三つのEmbeddef Frameworkが追加されています。

これは、先ほども上げた、アプリの構成図の黄色い四角で囲われた部分ごとに切り分けているものです。
ef5fc34c-7f99-4689-8f98-bcee1ff62735

特にこのEmbedded Frameworkに切り分けて実装せずにアプリのターゲットのみで開発することも可能なのですが、Clean Architectureの様な細かく責務が分けられた設計の場合、使うことのメリットが大きいのでこの方法を採用しました。

ビルドパフォーマンスの向上

Xcodeの差分コンパイルのが各フレームワークごとに効く様になるので、スコープが小さくなり、ビルド時間の短縮に繋がります。
ただ、Pokedexの場合はアプリ自体が大規模ではないので、そこまで恩恵は受けられてません。

不要な参照を制御

1つのターゲットに全てのファイルが入っていると、例えば、元来Presentationでは、DataStoreの処理を直接呼び出す必要がないにも関わらず、何の制限もなく呼び出すことができてしまいます。これでは、せっかく責務を切り分けたのにも関わらず、参照のルールが崩れてしまうので、図の通りにならなくなってしまいます。

自分一人だけで開発していれば、そうしない様に気を付けるだけで済みますが、複数人で開発している場合や設計に慣れていない人と一緒に開発する場合はそういうわけにはいきません。

そこをEmbedded Frameworkで切り分けることで、importしなければ、参照ができなくなるという仕様が活きてきます。

例えばPresenterでは、UseCaseの処理を呼び出すために、import Domainをしなければ、UseCaseを呼び出すことができなくなります。
スクリーンショット 2020-04-05 20 52 27

最初の例で言うならば、以下の画像の様なコードをレビューで発見した場合、怪しいコードとして心してかからなければいけないということです。
スクリーンショット 2020-04-05 20 37 42

コードレビューをした際に、こういった設計にしたがっていないコードを見落としてしまう可能性がありますが、Frameworkのターゲットを切り分けておくと、importを確認すれば、まずは参照のルールが守られているコードであるかどうかがわかるので、レビューの負担が少し減るというわけです。

あとは、少し副産物的なメリットですが、変換候補を減らすことができるという点もメリットになるでしょう。
Clean Architecutureを使用することで、ファイルや定義が非常に多くなってしまいます。(Pokedexの場合、PokemonDetailという名前を含んだ命名が32個も存在します。)

必要ない定義がFrameworkの切り分けによって少なくなるだけで、コーディングしている際のストレスがなくなります。

その32個の定義が気になる物好きの人はこちら
PokemonDetailResponse
PokemonDetailRequest
PokemonDetailAPIGatewayProvider
PokemonDetailAPIGateway
PokemonDetailAPIGatewayImpl
PokemonDetailRepositoryProvider
PokemonDetailRepository
PokemonDetailRepositoryImpl
PokemonDetailUseCaseProvider
PokemonDetailUseCase
PokemonDetailUseCaseImpl
PokemonDetailTranslatorProvider
PokemonDetailTranslator
PokemonDetailTranslatorImpl
PokemonDetailModel
PokemonDetailBuilder
PokemonDetailView
PokemonDetailViewController
PokemonDetailFavoriteButton
PokemonDetailSingleImageCell
PokemonDetailDualImageCell
PokemonDetailHeightCell
PokemonDetailWeightCell
PokemonDetailPokemonTypeCell
PokemonDetailPokemonTypeItemCell
PokemonDetailStatusCell
PokemonDetailPresenter
PokemonDetailPresenterImpl
PokemonDetailWireframe
PokemonDetailWireframeImpl
TransitToPokemonDetailWireframe

(これが変換に一気に出てくると思うとゾッとしますねw)

ライブラリの依存を明確にできる

あとは、ライブラリの依存を明確にできるということもメリットになります。
どういうことかというと、PokedexではAPI通信をする際にはAlamofireというライブラリを使用していますが、このライブラリはDataStoreの中でしか使用しません。

そういった場合に、この様に、DataStoreに対してCarthageでインストールしたライブラリをリンクしています。
スクリーンショット 2020-04-05 22 23 30

そうすると、使用する必要がないPresentationやDomainからライブラリの呼び出しを制限することができるので、ライブラリへの依存が明確になります。

コーディング編

最後に実際のコーディングに関して、最低限これはしておいて欲しいというものを紹介します。

アクセス修飾子

アクセス修飾子を正しく使うことで、より堅牢にコードを保持することができます。
全て知っておく必要がありますが、Pokedexにおいて主に使っているのはprivateinternalpublicの三つです。

他のも知っておきたい方は以下の記事を参考にしてください。
知っているようで知らないSwift5のアクセス修飾子

private

外部から参照されない(もしくはされたくない)クラスや変数には必ずprivateをつける様にしましょう。

import UIKit

final class PokemonListCell: UITableViewCell {

    @IBOutlet private weak var spriteImageView: UIImageView!

    @IBOutlet private weak var numberLabel: UILabel!

    @IBOutlet private weak var nameLabel: UILabel!

    func setData(_ data: PokemonListModel.Pokemon) {
        self.spriteImageView.loadImage(with: data.imageUrl, placeholder: Asset.mosnterball.image)
        self.numberLabel.text = "No.\(data.number)"
        self.nameLabel.text = data.name
    }
}

@IBOutletで紐づけられたpropertyはprivate付け忘れがちですが、継承して使うとかが無い限りはprivateを付けておきましょう。

import Foundation

public enum PokemonListAPIGatewayProvider {

    public static func provide() -> PokemonListAPIGateway {
        return PokemonListAPIGatewayImpl(dataStore: PokeAPIDataStoreProvider.provide())
    }
}

public protocol PokemonListAPIGateway {
    func get(completion: @escaping ((Result<PokemonListResponse, Error>) -> Void))
}

private struct PokemonListAPIGatewayImpl: PokemonListAPIGateway {

    let dataStore: PokeAPIDataStore

    func get(completion: @escaping ((Result<PokemonListResponse, Error>) -> Void)) {
        self.dataStore.request(PokemonListRequest(), completion: completion)
    }
}

PokemonListAPIGatewayImplはPokemonListAPIGatewayProviderのprovide()の中でしか呼び出す必要が無いので、privateをつけておくと、他のファイルから参照できない様になります。

public

Pokedexの場合、Embedded Frameworkを利用しているので、フレームワーク外から参照されるクラスに関してはpublicを指定する必要があります。

import DataStore
import Foundation

public enum PokemonListUseCaseProvider {

    public static func provide() -> PokemonListUseCase {
        return PokemonListUseCaseImpl(
            repository: PokemonListRepositoryProvider.provide(),
            translator: PokemonListTranslatorProvider.provide()
        )
    }
}

public protocol PokemonListUseCase {
    func get(completion: @escaping ((Result<PokemonListModel, Error>) -> Void))
}

private struct PokemonListUseCaseImpl: PokemonListUseCase {

    let repository: PokemonListRepository
    let translator: PokemonListTranslator

    func get(completion: @escaping ((Result<PokemonListModel, Error>) -> Void)) {
        self.repository.get { result in
            switch result {
            case .success(let response):
                completion(.success(self.translator.convert(from: response)))
            case .failure(let error):
                completion(.failure(error))
            }
        }
    }
}

PresentationのPokemonListPresenterImpl内で、PokemonListUseCaseが参照されているため、PokemonListUseCaseにはpublicを付ける必要があります。

PokemonListUseCaseProviderとその中のprovide()メソッドもPokemonListBuilderで呼び出されるのでpublicをつけましょう。

final修飾子

継承されないclassには必ずfinalを指定する様にしましょう。

import UIKit

final class PokemonDetailViewController: UIViewController {

}

これを付けることによって、継承の必要性が無いということを明示できるのでソースコードの理解に役立つ上に、実行時のパフォーマンス向上にもつながります。

Swiftのfinalについて

Decodable

PokeAPIはレスポンスがJsonなので、Codableという機能でレスポンスをパースすることができます。

その時に、Codableは、Encodable(オブジェクト -> Data型に変換する)とDecodable(Data型 -> オブジェクトに変換する)の両方に準拠してしまうので、今回の様なEncodableが必要が無い場合には、Decodableのみに準拠させると目的がはっきりするのでわかり易くなります。

import Foundation

public struct PokemonListResponse: Decodable {

    public let count: Int

    public let previous: String?

    public let next: String?

    public let results: [Result]
}

extension PokemonListResponse {

    public struct Result: Decodable {

        public let name: String

        public let url: String
    }
}

おわりに

最初、Pokedexを公開した時に誰も見てくれないだろうと思ってtwitterで呟いたら、予想以上の人に見てもらえたので、急いで解説記事を書きました🤯

至らぬところは多々ある上に、少し長く難しい内容になってしまったかもしれませんが、おそらくこの記事に書いている内容を実践して身につけていけば、業務未経験の人でもiOSエンジニアとして仕事を得るのに役立つ内容であると思います。

まだ少し書きたい事柄もあり、適度に加筆修正する予定なので、この記事についての質問も歓迎ですし、さらにはPokedexへのissue、pull requestもお待ちしております!

fr0g_fr0g
ダブルフロッグ
andfactory
Smartphone Idea Companyとして、人々の生活に「&(アンド)」を届ける。
https://andfactory.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away