Takahiro Octopress Blog

-1から始める情弱プログラミング

Clean Swiftを勉強してみよう!(1)

| Comments

はじめに

本日はClean Swiftについて書いていきたいと思います。

Clean Swiftとは

Clean Swiftは簡単に言うと『Clean ArchitectureのSwift版』です。
参考までにClean Architectureの有名な図を掲載します。
Clean Architectureより抜粋させて頂きました。
Clean Architecture

Clean Swiftアーキテクチャを採用することで受けられる恩恵として下記が考えられます。

  • 各種コンポーネントの責務を細分化することで、Massive ViewControllerの解消に繋がる
  • データの方向性が一方向になるため、各種コンポーネントの相互依存性が減り、TDD開発が進めやすくなる
  • 各種コンポーネントの責務がはっきりしているため、チーム開発する際に、実装が平準化される

コンポーネントの関係性

各種コンポーネントの関係性を表した全体像が下図になります。
Clean Swift Architectureの図

この関係性を説明するにあたって、各種コンポーネントの責務を理解しておく必要があるのでそれぞれ見ていきましょう。

各コンポーネントの説明

View

特に他のアーキテクチャと大きく違わない認識です。

責務:
① iOSアプリの見た目を表現する

ViewController

Massive ViewController になりがちな部分ですが、Clean Swiftでの責務は以下になります。

責務:
Interactor に具体的な処理内容(表示ロジック)を問い合わせる
Presenter からの指示を受けて、最適な View を描画する
Router に画面遷移を依頼する

具体例は下記になります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import UIKit

protocol SampleViewDisplayLogic: class {
  func displaySomething(viewModel: SampleView.Something.ViewModel)
  func displayError(viewModel: SampleView.Something.ViewModel)
  func transitionToSomeWhere(viewModel: SampleView.Sometime.ViewModel)
}

class SampleViewController: UIViewController, SampleViewDisplayLogic {
  var interactor: SampleViewBusinessLogic?
  var router: (NSObjectProtocol & SampleViewRoutingLogic & SampleViewDataPassing)?

  // MARK: Object lifecycle
  override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
    super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
    setup()
  }

  required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    setup()
  }

  // MARK: Setup
  private func setup() {
    let viewController = self
    let interactor = SampleViewInteractor()
    let presenter = SampleViewPresenter()
    let router = SampleViewRouter()
    viewController.interactor = interactor
    viewController.router = router
    interactor.presenter = presenter
    presenter.viewController = viewController
    router.viewController = viewController
    router.dataStore = interactor
  }

  // MARK: Routing
  override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if let scene = segue.identifier {
      let selector = NSSelectorFromString("routeTo\(scene)WithSegue:")
      if let router = router, router.responds(to: selector) {
        router.perform(selector, with: segue)
      }
    }
  }

  // MARK: View lifecycle
  override func viewDidLoad() {
    super.viewDidLoad()

    fetchSomethingOnLoad()
  }

  // ① Interactorに具体的な処理内容を問い合わせる
  func fetchSomethingOnLoad() {
    let request = SampleView.Something.Request()
    interactor?.fetchSomething(request: request)
  }

  // ② Presenterからの指示を受けてViewを描画する
  func displaySomething(viewModel: SampleView.Something.ViewModel) {
    // do something
  }

  func displayError(viewModel: SampleView.Something.ViewModel) {
    // do error something
  }

  // ③ Routerに画面遷移を依頼する
  func transitionToSomeWhere(viewModel: SampleView.Sometime.ViewModel) {
    // 画面遷移
    router?.routeToSomeWhere(segue: nil)
  }
}

また、ユーザによるアクション起因の場合は下記のようにするだけです。

1
2
3
4
5
@IBAction func tapSomeAction(_ sender: Any) {
  // ① Interactorに具体的な処理内容を問い合わせる
  let request = SampleView.Sometime.Request()
  interactor?.fetchSometime(request: request)
}

Presenter からの指示を受けて、 ViewController は描画処理を実行するため、見た目の整形などの 描画処理自体ViewController 内に書きます。

例えば、

・正方形の UIView を角丸にする/背景色を変更する/非表示にする etc
・マップにマーカを配置する/図形を描画する etc

Interactor

ViewController から依頼を受け、 Interactor は下記を実施する責務を持っています。

責務:
WorkerPresenter を仲介する
② どんな条件で、 Worker に何の処理を依頼するのかハンドリングする
Worker 経由で取得したレスポンスを Presenter に渡す

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import UIKit

protocol SampleViewBusinessLogic {
    func fetchSomething(request: SampleView.Something.Request)
    func fetchSometime(request: SampleView.Sometime.Request)
}

protocol SampleViewDataStore {
  // 画面遷移時にパラメータを受け取れるように定義
  var something: String { get set }
}

class SampleViewInteractor: SampleViewBusinessLogic, SampleViewDataStore {
    var presenter: SampleViewPresentationLogic?
    var worker = SampleViewWorker?
    var something: String!

    func fetchSomething(request: SampleView.Something.Request) {
      // ① WorkerとPresenterを仲介する
      worker.fetch(success: { (object) in
        // 処理が成功した場合
        // ③ Worker経由で取得したレスポンスをPresenterに渡す  
        let response = SampleView.Something.Response(object: object, isError: false)
        self.presenter?.presentSomething(response: response)
      }, failure: { _ in
        // 処理が失敗した場合
        // ③ Worker経由で取得したレスポンスをPresenterに渡す  
        let response = SampleView.Something.Response(object: object, isError: true)
        self.presenter?.presentSomething(response: response)
      })

    func fetchSometime(request: SampleView.Sometime.Request) {
      // ② どんな条件で、Workerに何の処理を依頼するのかハンドリングする
      if request.time > Date() {
        let response = SampleView.Sometime.Response(future: true)
        presenter?.presentSometime(response: response)

        return
      }
      let response = SampleView.Sometime.Response(future: false)
      presenter?.presentSometime(response: response)
    }

    func fetchSomeWhat(request: SampleView.SomeWhat.Request) {
      // 画面遷移時に渡されたパラメータを利用した描画を実施したい場合
      let response = SampleView.Something.Response(object: something)
      self.presenter?.presentSomething(response: response)
    }
}

Worker

Interactor から受けた依頼を実行します。

責務:
API 処理や Core Data / Realm などのアプリ内ローカルデータの処理をハンドリングする
② 成功/失敗レスポンスをハンドリングする

1
2
3
4
5
6
7
8
9
10
11
12
13
import UIKit

class SampleViewWorker {

    func fetch(success: @escaping ((SomeObject) -> Void), failure: @escaping ((Error) -> Void)) {
      // APIリクエストまたはローカルDBへのアクセスを実行してデータを取得
      // 具体的な処理は省略
      let obj: SomeObject = ...
      success(obj)
    }) { (error) in
      failure(error)
    }
}

Presenter

Interactor から Worker 経由で取得したレスポンスを受け取った後に、 Presenter は下記を実行することを責務とします。

責務:
① 受け取ったレスポンスを元に最適な表示(成功/失敗などの表示)になるようハンドリングする
② 受け取ったレスポンスを Model.ViewModel 形式に変換する
ViewControllerModel.ViewModel を渡して描画を依頼する

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import UIKit

protocol SampleViewPresentationLogic {
  func presentSomething(response: SampleView.Something.Response)
}

class SampleViewPresenter: SampleViewPresentationLogic {
  weak var viewController: SampleViewDisplayLogic?

  // MARK: Present something

  func presentSomething(response: SampleView.Something.Response) {
    // ② 受け取ったレスポンスをModel.ViewModel形式に変換する
    let viewModel = SampleView.Something.ViewModel(object: response.object)

    // ① 受け取ったレスポンスを元に最適な表示(成功/失敗などの表示)になるようハンドリングする
    if response.isError {
      // エラーがある場合
      // ③ ViewControllerにModel.ViewModelを渡して描画を依頼する  
      viewController?.displayError(viewModel: viewModel)
      return
    }

    // ③ ViewControllerにModel.ViewModelを渡して描画を依頼する
    viewController?.displaySomething(viewModel: viewModel)
  }
}

Model

Clean Swiftアーキテクチャの肝といっても過言ではないのが Model です。

責務:
① 各種コンポーネントを切り離し、各種コンポーネント間のやり取りに利用される
Request / Response / ViewModel の3つの構造体を持つ

3つの構造体の説明:
Request
  ・ ユーザの操作をInputパラメータとして内包したデータ形式
  ・ ViewController から Interactor に渡される
Response
  ・ Worker 処理結果を内包しているデータ形式
  ・ Interactor から Presenter に渡される
ViewModel
  ・ ViewController での描画に即したデータ形式
  ・ Presenter から ViewController に渡される

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import UIKit

enum SampleView {

  // MARK: Fetch something
  enum Something {
    struct Request {
    }
    struct Response {
      var object: SomeObject
      var isError: Bool
    }
    struct ViewModel {
      var object: SomeObject
    }
  }

  // MARK: Fetch sometime
  enum Sometime {
    struct Request {
      ...
    }
    struct Response {
      ...
    }
    struct ViewModel {
      ...
    }
  }
}

データフローの例

各コンポーネントの責務を理解した上で、コンポーネント間のフローの流れを見ていきましょう。

ユーザがボタンをタップして通信処理後に取得データを描画するフロー

ユーザが View 上のボタンをタップした後に、外部APIを叩いて取得したデータで View 描画するデータフローは下記になります。

ViewControllerView に対するユーザアクションを検知
ViewControllerInteractorModel.Request を送って具体的な処理を依頼
InteractorWorker に処理を依頼
Worker が通信処理した結果を Interactor に返却
Interactor が返却データを Model.Response に変換して、 Presenter に処理を依頼
Presenter が受け渡されたデータを Model.ViewModel に変換して、 ViewController に描画を指示
ViewControllerView に描画を反映

データのフロー例1

※このフローでは画面遷移がないため、Routerへの繋がりはありません。

ユーザがボタンをタップしてローカルDBからデータ取得して画面遷移するフロー

ユーザが View 上のボタンをタップした後に、ローカルDB内データを取得して、画面遷移するデータフローは下記になります。

ViewControllerView に対するユーザアクションを検知
ViewControllerInteractorModel.Request を送って具体的な処理を依頼
InteractorWorker に処理を依頼
Worker がローカルDBから処理した結果を Interactor に返却
Interactor が返却データを Model.Response に変換して、 Presenter に処理を依頼
Presenter が受け渡されたデータを Model.ViewModel に変換して、 ViewController に描画を指示
ViewControllerRouter に画面遷移を依頼
Router が依頼された画面先にデータを受け渡し、画面遷移を実行

データのフロー例2

まとめ

まず、各種コンポーネントの責務と、そのコンポーネント間の関係性および抽象的なデータフローについて説明しました。
次回は具体的なサンプルを元にClean Swiftについて説明したいと思います。

参考URL:

Comments