【swift】フローチャートを管理するクラスを作る

## 概要
アプリに通信処理等を実装していると、取得データの表示や、エラー処理、続きのデータを取得、またはデータを取得する際にログインが必要等、様々な分岐処理が必要になってくることがあります。
それを全てUIを管理するViewControllerに書いてしまうと、分岐条件がわかりにくくなったり、ネストが深くなってしまったり、様々なところに処理のスタート処理が存在してしまったりと、非常にわかりにくい管理になってしまいます。

そのような問題を回避するため、処理フローが多岐にわたる場合は、フローチャートを管理するクラスを作成して処理のフローはそのクラスで行い、UIは表示だけを行うようにすることが出来ます。


今回は、youtube Data APIでyoutubeのプレイリスト情報を取得することを想定したフローチャートクラスを作成してみようと思います。実際にyoutubeからAPIを実行してDataを取得するわけではありませんのでyoutube Data APIの参考にはなりません。

#### youtube Data API for iOS
https://developers.google.com/youtube/v3/quickstart/ios

## フローチャート

<img src="https://qiita-image-store.s3.amazonaws.com/0/194104/f27cd5cf-4112-9789-7cd6-fc54a753ab81.png" width=60%>

※ 通信エラー等を考慮していません。

はじめにログイン時に保有しているアクセストークンが有効か調べます。
アクセストークンが有効なら、そのアクセストークを利用してプレイリスト情報を取得します。
アクセストークンが無効なら、ログイン画面を表示して、ログインを行いアクセストークンを取得してプレイリスト情報を取得します。
プレイリスト情報の取得後、続きの情報がある場合は再度プレイリスト情報を取得します。
続きの情報がない場合はそこで終了です。

## サンプルコード

### UI

メイン画面ではflowを呼び出し、プレイリストの表示とログイン画面の表示とログイン画面を閉じる作業を行います。
続きのプレイリストがある場合は、tableViewの最後のセルが表示されると続きのプレイリスト情報が取得されます。
Flowを再開する時は、メイン画面から flow.doReserveTask()を呼び出します。

```swift:ViewController.swift(メイン画面)
import UIKit

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, LoginDelegate {

    @IBOutlet weak var tableView: UITableView!
    // flow
    let flow = Flow()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        
        // Flowの開始
        flow.startFlow(failed: { (error) in
            // エラーアラート等
            
        }, dataCompletion: { 
            // TableViewをリロード
            self.tableView.reloadData()
        }, needLogin: {
            // ログイン画面を開く
            self.performSegue(withIdentifier: "login", sender: nil)
        }, loginSuccess:{
            // ログイン画面を閉じてプレイリスト情報を取得する
            self.presentedViewController?.dismiss(animated: true, completion: {
                self.flow.doReserveTask()
            })
        })
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        if flow.isNext {
            // 続きの取得情報がある場合は、最後のセルを続きのプレイリスト取得セルにする
            return flow.playlist.count + 1
        } else {
            return flow.playlist.count
        }
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = UITableViewCell(style: .default, reuseIdentifier: "cell")
        if indexPath.row == flow.playlist.count {
            // 最後のセルが表示されたら次の情報を取得する
            flow.doReserveTask()
        } else {
            // セルに内容を表示
            cell.textLabel?.text = flow.playlist[indexPath.row]
        }
        return cell
    }
    
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "login" {
            let lvc = segue.destination as! LoginViewController
            lvc.delegate = self
        }
    }
    
    func login() {
        // フローでログイン処理を実行
        flow.doReserveTask()
    }
}
```

ログイン画面はログインボタンの表示とその実行のみを行います。
ログインの処理は、メイン画面からFlowクラスでflow.doReserveTask()を呼び出して実行します。

```swift:LoginViewController.swift(ログイン画面)
import UIKit

// MARK: Delegate
protocol LoginDelegate : NSObjectProtocol {
    func login()
}

class LoginViewController: UIViewController {
    
    var delegate: LoginDelegate?
    
    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
    }
    
    @IBAction func login(_ sender: Any) {
        delegate?.login()
    }
}
```

### Flow

続けてtaskを実行する場合はdoNext()で次のtaskを実行し、ログイン処理待ちや、続きのプレイリスト情報の取得待ちの状態では、次のtaskをsetReserve()にセットし、doReserveTask()が呼ばれるまで待機します。

```swift:Flow.swift
import UIKit

class Flow: NSObject {

    let manager = DataManager()
    
    // task
    var reserve: Selector?
    
    // callback
    var failed: ((Error) -> Void)?
    var dataCompletion: (() -> Void)?
    var needLogin: (() -> Void)?
    var loginSuccess: (() -> Void)?
    
    // argument
    var offset: Int = 0
    
    // プレイリスト情報
    var playlist: [String] = []
    // 続きのプレイリスト情報があるかどうか
    var isNext = false
    
    // MARK: Flow Task
    
    /**
     *  Flowを開始するときに呼ばれる
     */
    func startFlow(failed: ((Error) -> Void)?,
                   dataCompletion: (() -> Void)?,
                   needLogin: (() -> Void)?,
                   loginSuccess: (() -> Void)?) {
        self.failed = failed
        self.dataCompletion = dataCompletion
        self.needLogin = needLogin
        self.loginSuccess = loginSuccess
        // はじめのタスクを実行
        doNext(#selector(validateAccessToken))
    }

    /**
     *  AccessTokenのvalidationを行う
     *  AccessTokenが有効の場合は、プレイリスト情報を取得する
     *  AccessTokenが無効の場合は、ログイン画面を表示する
     */
    @objc func validateAccessToken() {
        manager.validateAccessToken(completion: { [unowned self] (error) in
            if error == nil {
                // accessTokenが有効の場合は、プレイリスト情報を取得する
                doNext(#selector(self.getPlaylistData))
            } else {
                // 期限切れのaccessTokenの場合はログイン画面を表示する（※ネットワークエラー等との区別が必要）
                setReserve(#selector(self.login))
                // UIにログイン画面を表示するように指示
                needLogin?()
            }
        })
    }
    
    /**
     *  AccessTokenが無効の場合にログイン処理を行う
     *  ログインに成功した場合は、プレイリスト情報を取得する
     *  ログインに失敗した場合は、エラーアラート等を表示する
     */
    @objc func login() {
        manager.login(completion: { [unowned self] (error) in
            if error == nil {
                // loginに成功したらreserveをnilにする
                reserve = nil
                // 次のタスクをセット
                setReserve(#selector(self.getPlaylistData))
                // ログイン成功をUIに通知
                loginSuccess?()
            } else {
                // ログイン失敗アラート等
                failed?(error!)
            }
        })
    }
    
    /**
     *  プレイリスト情報を取得する
     *  プレイリスト情報の取得に成功した場合は、UIにプレイリスト情報を送る
     *  続きの情報がある場合は、UIに続きの情報があることを知らせる
     */
    @objc func getPlaylistData() {
        manager.getPlaylistData(offset: offset, completion: { [unowned self] (playlist, isNext, error) in
            if error == nil {
                // 取得に成功したらreserveをnilにする
                reserve = nil
                // データが取得できたらtableViewに表示する
                self.playlist = self.playlist + playlist
                self.isNext = isNext
                if isNext {
                    // 次に取得するオフセット値を設定する
                    offset = self.playlist.count
                    // 次のタスクをセット
                    setReserve(#selector(self.getPlaylistData))
                }
                // データをUIに送信
                dataCompletion?()
            } else {
                // 取得失敗
                failed?(error!)
            }
        })
    }
    
    // MARK: Private Method
    
    private func doNext(_ selector: Selector) {
        perform(selector)
    }
    
    private func setReserve(_ selector: Selector) {
        reserve = selector
    }
    
    // MARK: Public Method
    
    func doReserveTask() {
        guard reserve != nil else { return }
        perform(reserve)
    }
}

```

## サンプルプロジェクト
GitHubにプロジェクトを公開しています。
https://github.com/nadioo/FlowSample

## 開始地点が複数ある場合

FlowクラスのstartFlowメソッドの引数に始めに実行するtaskを追加する。

```swift:Flow.swift
    /**
     *  Flowを開始するときに呼ばれる
     */
    func startFlow(task: Selector,
                   failed: ((Error) -> Void)?,
                   dataCompletion: (() -> Void)?,
                   needLogin: (() -> Void)?,
                   loginSuccess: (() -> Void)?) {
        self.failed = failed
        self.dataCompletion = dataCompletion
        self.needLogin = needLogin
        self.loginSuccess = loginSuccess
        // はじめのタスクを実行
        doNext(task)
    }
```

Flow開始時にSelectorを引数で渡す。#selector(Flow.validateAccessToken)

```swift:ViewController.swift(メイン画面)
        // Flowの開始
        flow.startFlow(task: #selector(Flow.validateAccessToken), failed: { (error) in
            // エラーアラート等
            
        }, dataCompletion: { 
            // TableViewをリロード
            self.tableView.reloadData()
        }, needLogin: {
            // ログイン画面を開く
            self.performSegue(withIdentifier: "login", sender: nil)
        }, loginSuccess:{
            // ログイン画面を閉じてプレイリスト情報を取得する
            self.presentedViewController?.dismiss(animated: true, completion: {
                self.flow.doReserveTask()
            })
        })
```