[iOS] コードで覚えるクリーンアーキテクチャ 〜VP部分を書いてみよう〜

2016.11.21

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

おばんです、実家から送られてくる食材や食べ物、また友達からおすそ分けしてもらう実家から送られた野菜などにとても生かされています田中です。
食材本当にありがたい。

はじめに

この記事は先日登壇させていただいたiOSオールスターズ2の発表内容と関連した内容となっています。
合わせてお読みいただけると幸いです。

イベントに関してはこちらになります。

ソースコードはこちらになります。

今記事の目標

今回の記事はクリーンアーキテクチャになじみのないiOSエンジニアの方を対象に、iOSアプリをクリーンアーキテクチャでViewControllerとPresenterの部分に限定して慣れていただけることを目標にします。
クリーンアーキテクチャの意義、メリットデメリットなどに関しては軽くおさらいする程度として実際のソースコードを追いながらまずどんなものなのかのイメージがつくように書いていきます。

敷居の高いクリーンアーキテクチャ

正直クリーンアーキテクチャは敷居が高い...!!
クリーンアーキテクチャに関して名前は知っていて、ちょっと気になる気持ちから世の中にある記事を読んでみたみなさんは、きっとこうなりましたよね?

G5pfP

クリーンアーキテクチャに関する記事は多々ありますが、実際それを読み解きながら書いてみた筆者の経験からも理解が容易でないと感じました。
その経験もあったので、この記事においては小さな機能を、まずライトなクリーンアーキテクチャの捉え方で実装していきます。

iOSにおけるクリーンアーキテクチャとは?

クリーンアーキテクチャは端的に言うと依存関係を単一方向とすることでクラス間の独立性を高めたアプリケーション設計です。
同時に、その依存関係の方向を決定づけるために役割を細かく分割し、役割ごとに責務を明確にするアプリケーション設計でもあります。

メリット

  • 機能追加・仕様変更(未確定事項)に強い
  • 設計の基準ができる
  • 責務の分散・明確化
  • テストがしやすくなる

など。

デメリット

  • コード量が多い
  • ファイル量が多い
  • 難易度・敷居が高い

など。
クリーンアーキテクチャを本気で書こうとすると役割が多かったり、クラス間の独立性を高めるためにインターフェースが増えていったりするのでどうしてもコード量・ファイル量が増えてしまい開発コストが高くなります。
そのため、中・長期の規模の開発プロジェクトにオススメします。

登場する役割の説明

今回実装するのはこの部分です。
Presentation層以外の層と役割については別記事にて取り扱う予定です。

スクリーンショット 2016-11-21 14.11.47

スクリーンショット 2016-11-21 16.28.27

Presentation Layer

UIの表示やユーザーからのイベントのハンドリングを担当するレイヤーです。

スクリーンショット 2016-11-21 14.41.16

Interface(Presentation層の)

  • PresenterからVCに対する指示をまとめたProtocol
  • Protocolの実装はViewControllerが行う

ViewController

  • ユーザーからの操作などのタイミングで、Presenterに必要な指示を出す
  • PresenterからInterface経由で送られた指示に対して適切なView操作を行う

Presenter

  • Domain層(ビジネスロジック)とPresentation層の橋渡し
  • Viewに
必要なデータをDomain層から持ってきて、描画に必要なロジックを取りまとめてViewControllerに伝える

VPとして役割を分ける目的

今回取り扱うVP部分ですが、View操作とView操作に関するロジックを切り分けることを目的としています。

ビジネスロジックから返された処理結果をもとに、このViewはisHiddenをtrueにしてこのViewはfalseにしてとか。
ViewControllerが直接にそのロジックとView操作を管理しだすと「あっちではViewをこの状態にして、このロジックの場合だとあの状態にして...」と複雑で肥満体質なViewControllerになってしまいます。コードも読みづらくなります。
それを避けるためにPresenterに表示系のロジックを切り出すのです。

VP部分を書いてみよう

今回紹介するサンプルはTwitterのタイムラインを表示するプロジェクトです。
作る画面は以下の三つです。

  • ログイン画面
  • タイムライン画面
  • EmptyState画面

login

スクリーンショット_2016-11-21_13_57_44

スクリーンショット_2016-11-21_13_52_50

コード全文の中から、役割を担っている部分を見ていきます。

Interfaceの解説

Interfaceの役割は以下となります。

  • PresenterからVCに対する指示をまとめたProtocol
  • Protocolの実装はViewControllerが行う
protocol TimelineVCOutput: class {
    func showEmptyView()
    func hideEmptyView()
    func showTimeline(timeline: Timeline)
}

単純なインターフェースなのでシンプルです。
Presenterがここに定義されているメソッドを呼び出すことで、実装先となるViewControllerが適切なView操作をします。

ViewControllerの解説

ViewControllerの役割は以下の二つです。

  • ユーザーからの操作などのタイミングで、Presenterに必要な指示を出す
  • PresenterからInterface経由で送られた指示に対して適切なView操作を行う

ユーザーからの操作などのタイミングで、Presenterに必要な指示を出す

class TimelineViewController: UIViewController {

    @IBOutlet weak var tableView: UITableView!
    @IBOutlet weak var emptyView: UIView!
    
    fileprivate var timeline: Timeline? = nil {
        didSet {
            tableView.reloadData()
        }
    }
    
    private var timelinePresenter: TimelinePresenter!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        setupTimelinePresenter()
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        // HACK: 遷移に関するロジックを持ってしまっていることに関しては別記事にて取り扱う
        if Account.twitterAccount == nil {
            transitionToLoginViewController()
        }
    }
    
    // 
    private func setupTimelinePresenter() {
        timelinePresenter = TimelinePresenter(output: self)
    }
    
    // HACK: VC内でVCを持つことに関しては別記事にて取り扱う
    private func transitionToLoginViewController() {
        guard let loginVC = storyboard?.instantiateViewController(withIdentifier: "LoginViewController") as? LoginViewController else { return }
        
        loginVC.didLogin = { [weak self] in
            self?.timelinePresenter.getTimeline()
        }
        
        present(loginVC, animated: true, completion: nil)
    }

}

以下のdidLoginの中で設定されているgetTimeline()がpresenterに対して指示を出しているところが該当箇所になります。
ログイン完了時に、「タイムラインを取得する」という指示をPresenterに出しています。

private func transitionToLoginViewController() {
        guard let loginVC = storyboard?.instantiateViewController(withIdentifier: "LoginViewController") as? LoginViewController else { return }
        
        loginVC.didLogin = { [weak self] in
            self?.timelinePresenter.getTimeline()
        }
        
        present(loginVC, animated: true, completion: nil)
    }

コード中のコメントにある画面遷移に関する部分は以下の記事で言及しています。
まだ実験中ですが、もし参考になることがありそうであればお読みください。

PresenterからInterface経由で送られた指示に対して適切なView操作を行う

Presenterから指示された実装がここにまとめられています。
ロジックはここでは含まないので、「このViewにこの操作を行う」というだけのシンプルな構成です。

extension TimelineViewController: TimelineVCOutput {
    func showEmptyView() {
        emptyView.isHidden = false
    }
    
    func hideEmptyView() {
        emptyView.isHidden = true
    }
    
    func showTimeline(timeline: Timeline) {
        self.timeline = timeline
    }
}

Presenterの解説

Presenterの役割は以下の二つです。

  • Domain層(ビジネスロジック)とPresentation層の橋渡し
  • Viewに
必要なデータをDomain層から持ってきて、描画に必要なロジックを取りまとめてViewControllerに伝える
class TimelinePresenter {
    
    private weak var timelineVCOutput: TimelineVCOutput!
    // ビジネスロジックを受け持つクラス
    private let timelineUseCase = TimelineUseCase()
    
    init(output timelineVCOutput: TimelineVCOutput) {
        self.timelineVCOutput = timelineVCOutput
    }
    
    func getTimeline() {
        timelineUseCase.getTimeline { [weak self] result in
            switch result {
            case .failure(let error):
                self?.timelineVCOutput.showEmptyView()
            case .success(let timeline):
                self?.timelineVCOutput.hideEmptyView()
                self?.timelineVCOutput.showTimeline(timeline: timeline)
                self?.showEmptyViewIfNeeded(timeline: timeline)
            }
        }
    }
    
    private func showEmptyViewIfNeeded(timeline: Timeline) {
        if timeline.numberOfItem == 0 {
            timelineVCOutput.showEmptyView()
        }
    }
    
}

特に見ていただきたいのが以下です。
この箇所にViewの描画ロジックがすべて集約されているため、処理の流れが追いやすい作りになっています。

func getTimeline() {
    timelineUseCase.getTimeline { [weak self] result in
        switch result {
        case .failure(let error):
            self?.timelineVCOutput.showEmptyView()
        case .success(let timeline):
            self?.timelineVCOutput.hideEmptyView()
            self?.timelineVCOutput.showTimeline(timeline: timeline)
            self?.showEmptyViewIfNeeded(timeline: timeline)
        }
    }
}

まとめ

理論のみで追うとなかなか理解の難しいと、自分で学びながらも思ったので記事に起こしました。
今回はVP部分のみになりますが、それ以降のDomain層とData層も深い世界が広がっています。
また、クリーンアーキテクチャの意義として役割同士の独立性やDIに関する話もありますが、そこについても自分の中でまとまり次第またコードを追う形の記事を起こしてみたいと考えております。

サンプルコード作成にあたり、Taiki Suzuki(marty-suzuki)様よりGitHubでプルリクエストをいただきました。これにより、より良いコードを紹介出来ることができました。
ありがとうございます!

参考・関連