[Swift] FirebaseでToDoリストを作ってみた for clean architecture

# はじめに
Firebaseを勉強したので、簡単なToDoリストを作ってみます。

## 方針
・設計手法は、クリーンアーキテクチャを採用
・データの管理は、FirebaseのRealtime Databaseを採用

## 前提知識
・[Firebaseの導入方法](http://qiita.com/eKushida/items/54b62d3961a5c74a71af)
・[Firebaseによる認証方法](http://qiita.com/eKushida/items/5fe8fff256f8870f5be7)
・[FirebaseによるRealtime Databaseの使い方](http://qiita.com/eKushida/items/87e3f50d7e637ebf3d03)

## ゴール
ユーザごとにToDoリストを管理するアプリを作ります。
画面構成は、ログイン画面、ToDo一覧画面と、ToDo登録画面です。
（会員登録機能、ログアウト機能については、割愛します。）

### （１）ログイン画面
・メールアドレス、パスワードを入力し、ログインボタンを押下すると、ToDo一覧画面へ遷移する

<img width="302" alt="スクリーンショット 2016-08-07 9.45.56.png" src="https://qiita-image-store.s3.amazonaws.com/0/71694/333edb4f-0048-6d09-b45e-441c0d434c38.png">

### （２）ToDo一覧画面
・ToDo一覧を表示する
・該当行をタップするとToDoを完了済みとする（取り消し線で消す）
・プラスボタンを押下すると、ToDo登録画面へ遷移する

<img width="294" alt="スクリーンショット 2016-08-07 9.46.23.png" src="https://qiita-image-store.s3.amazonaws.com/0/71694/96ae0987-8215-7314-5d76-57c263fd15ad.png">

### （３）ToDo登録画面
・タイトルを入力し、登録ボタンを押下すると、ToDo一覧画面に戻る
・バックボタンを押下すると、ToDo一覧画面に戻る

<img width="299" alt="スクリーンショット 2016-08-07 9.46.31.png" src="https://qiita-image-store.s3.amazonaws.com/0/71694/d1f8ead3-300f-0dc8-1596-f52c838a6b08.png">

## クラスの構成
### 1. UI層
#### 1.1. View
  - Login.Storyboard
  - ToDoList.Storyboard
  - ToDoAdd.Storyboard

#### 1.2. ViewController
  - LoginViewController.swift
  - ToDoListViewController.swift
  - ToDoAddViewControlle.swift

#### 1.3. Presenter
  - LoginPresenter.swift
  - ToDoListPresenter.swift
  - ToDoAddPresenter.swift 

### 2. Domain層

#### 2.1. Usecase
 - AuthenticationUsecase.swift
 - ToDoUsecase.swift

#### 2.2. Repository
 - AuthenticationRepository.swift
 - ToDoRepository.swift

#### 2.3. Entity
 - ToDo.swift

### 3. Data層
#### 3.1. Repository 
 - AuthenticationRepositoryImpl.swift
 - ToDoRepositoryImpl.swift

# 実践
それでは、作ってみます。

## 1. UI層
### 1.1. View
UIは、Storyboardで作っていきます。

Login.Storyboard

<img width="734" alt="スクリーンショット 2016-08-07 9.52.02.png" src="https://qiita-image-store.s3.amazonaws.com/0/71694/dc02324e-06e0-1581-c7e4-369358803ecf.png">

ToDoList.Storyboard

<img width="791" alt="スクリーンショット 2016-08-07 11.37.29.png" src="https://qiita-image-store.s3.amazonaws.com/0/71694/dc8323ae-f2cb-ee98-b848-1ba3eb5b2210.png">


ToDoAdd.Storyboard

<img width="792" alt="スクリーンショット 2016-08-07 11.39.19.png" src="https://qiita-image-store.s3.amazonaws.com/0/71694/9ecc4416-b4d9-b163-1869-8f5be6d4010b.png">


#### 1.2. ViewController
ログイン画面のViewControllerクラスです。
基本的に処理はすべてPresenterに委譲します。
（バリデーションチェック等のエラー処理は割愛しています）

```swift:LoginViewController.swift
import UIKit

protocol ToDoLoginViewInput {
    func success(uid: String)
    func failure(error: NSError)
}

class LoginViewController: UIViewController {
    
    @IBOutlet weak var mailAddressTextField: UITextField!
    @IBOutlet weak var passwordTextField: UITextField!
    
    let presener = LoginPresenter()
    
    //MARK:- LifeCycle
    override func viewDidLoad() {
        super.viewDidLoad()
        presener.viewDidLoad(self)
    }
    
    //MARK:- Actions
    @IBAction func didTapLogin(sender: UIButton) {
        
        if let mailAddress = mailAddressTextField.text,
            let password = passwordTextField.text {
            presener.login(mailAddress, password: password)
            return
        }
        
        //TODO: エラー処理
    }
}

extension LoginViewController: ToDoLoginViewInput {
    
    func success(uid: String) {
        presener.showToDoListViewController(uid)
    }
    
    func failure(error: NSError) {
        
        //TODO: エラー処理
        print(error.localizedDescription)
    }
}
```

ToDo一覧画面のViewControllerクラスです。
こちらも同様に、処理をPresenterに委譲しています。
また合わせてテーブルビューの構築を行っています。

```swift:ToDoListViewController.swift
import UIKit

protocol ToDoListViewInput {
    func setStatus(status: ToDoStatus)
    func success(todoItems: [ToDo])
    func failure(error: NSError)
}

class ToDoListViewController: UIViewController {
    
    @IBOutlet weak var tableView: UITableView!
    @IBOutlet weak var indicator: UIActivityIndicatorView!
    
    let presenter = ToDoListPresenter()
    var uid = ""
    var todoList = [ToDo]() {
        didSet {
            tableView.reloadData()
        }
    }
    
    //MARK:- LifeCycle
    override func viewDidLoad() {
        super.viewDidLoad()
        presenter.viewDidLoad(self, uid: uid)
    }
    
    override func viewWillAppear(animated: Bool) {
        super.viewWillAppear(animated)
        presenter.viewWillAppear()
    }
    
    //MARK:- Actions
    @IBAction func didTapAddTodo(sender: UIBarButtonItem) {
        
        if let vc = presenter.todoAddViewController() {
            vc.uid = uid
            self.navigationController?.pushViewController(vc, animated: true)
        }
    }
}

extension ToDoListViewController: ToDoListViewInput {
    
    func setStatus(status: ToDoStatus) {
        presenter.configureIndicator(indicator, status: status)
    }
    
    func success(todoItems: [ToDo]) {
        todoList.removeAll()
        todoList = todoItems
    }
    
    func failure(error: NSError) {
        print(error.description)
    }
}

extension ToDoListViewController: UITableViewDataSource {
    
    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return todoList.count
    }
    
    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        
        let cell = UITableViewCell(style: UITableViewCellStyle.Subtitle, reuseIdentifier: "Cell")
        presenter.configureCell(cell, todo: todoList[indexPath.row])
        return cell
    }
}

extension ToDoListViewController: UITableViewDelegate {
    
    func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
        tableView.deselectRowAtIndexPath(indexPath, animated: true)
        presenter.didTapDone(indexPath, todo: todoList[indexPath.row])
    }
}
```

ToDo登録画面のViewControllerクラスです。
こちらも同様に処理をPresenterに委譲しています。

```swift:ToDoAddViewController.swift
import UIKit

class ToDoAddViewController: UIViewController {
    
    @IBOutlet weak var titleTextField: UITextField!
    let presener = ToDoAddPresenter()
    var uid = ""
    
    //MARK:- LifeCycle
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    //MARK:- Actions
    @IBAction func didTapAddTodo(sender: UIButton) {
        
        if let title = titleTextField.text where !title.isEmpty {
            presener.addTodo(uid, todo: ToDo(title: title, isDone: false))
            self.navigationController?.popViewControllerAnimated(true)
            return
        }
        
        //TODO: バリデーションチェック
    }
}
```

#### 1.3. Presenter
ログイン画面のPresenterクラスです。
ログイン処理をUsecaseに委譲し、ログイン結果が返ってくると、LoginViewControllerに戻します。

```swift:LoginPresenter.swift
import UIKit

protocol LoginComplateDelegate {
    func success(uid: String)
    func failure(error: NSError)
}

class LoginPresenter: NSObject {
    
    let repository = AuthenticationRepositoryImpl()
    var loginViewInput: ToDoLoginViewInput?
    var viewController: LoginViewController?
    
    func viewDidLoad(viewController: LoginViewController) {
        self.viewController = viewController
        loginViewInput = viewController
        repository.loginDelegate = self
        
        if uid() != "" {
            showToDoListViewController(uid())
        }
    }
    
    func login(mailAddress: String, password: String) {
        
        AuthenticationUsecase(repository: repository).login(mailAddress, password: password)
    }
    
    func showToDoListViewController(uid: String) {
        if let vc = todoListViewController(), let viewController = viewController {
            vc.uid = uid
            viewController.navigationController?.pushViewController(vc, animated: true)
        }
    }
    
    private func uid() -> String {
        return AuthenticationUsecase(repository: repository).uid()
    }
    
    private func todoListViewController() -> ToDoListViewController?{
        return UIStoryboard.getViewController("ToDoList", identifier: "ToDoListViewController") ?? nil
    }
}

extension LoginPresenter: LoginComplateDelegate {
    
    func success(uid: String) {
        loginViewInput?.success(uid)
    }
    
    func failure(error: NSError) {
        loginViewInput?.failure(error)
    }
}
```

ToDo一覧用のPresenterクラスです。
こちらも、Usecaseに処理を委譲や、Viewに表示する値を加工をします。
また、ToDoリストが返ってくると、ToDoListViewControllerに戻します。

```swift:ToDoListPresenter.swift
import UIKit

enum ToDoStatus {
    case Loading
    case Done
    case Error
}

protocol ToDoListLoadComplateDelegate {
    func success(todoItems: [ToDo])
    func failure(error: NSError)
}

class ToDoListPresenter: NSObject {
    
    let repository = ToDoRepositoryImpl()
    var viewInput: ToDoListViewInput?
    var uid = ""
    
    //MARK:- Public Method
    func viewDidLoad(viewController: ToDoListViewController, uid: String) {
        viewController.tableView.dataSource = viewController
        viewController.tableView.delegate = viewController
        viewInput = viewController
        repository.todoListDelegate = self
        self.uid = uid
    }
    
    func viewWillAppear() {
        todoList()
    }
    
    func configureCell(cell: UITableViewCell, todo: ToDo) {
        cell.textLabel?.text = todo.title
        
        guard todo.isDone else{
            return
        }
        let attributeText = NSMutableAttributedString(string: todo.title)
        
        attributeText.addAttribute(NSStrikethroughStyleAttributeName,
                                   value: NSUnderlineStyle.StyleSingle.rawValue,
                                   range: NSMakeRange(0, todo.title.characters.count))
        
        cell.textLabel?.attributedText = attributeText
    }
    
    func configureIndicator(indicator: UIActivityIndicatorView, status: ToDoStatus) {
        
        switch status {
        case .Loading:
            indicator.hidden = false
            indicator.startAnimating()
            
        default:
            indicator.hidden = true
            indicator.stopAnimating()
        }
    }
    
    func didTapDone(indexPath: NSIndexPath, todo: ToDo) {
        
        let mutableToDo = ToDo(key: todo.key, title: todo.title, isDone: todo.isDone ? false : true)
        ToDoUsecase(repository: repository).update(uid, todo: mutableToDo)
        todoList()
    }
    
    func todoAddViewController() -> ToDoAddViewController?{
        return UIStoryboard.getViewController("ToDoAdd", identifier: "ToDoAddViewController") ?? nil
    }
    
    //MARK:- Private Method
    private func todoList() {
        viewInput?.setStatus(.Loading)
        ToDoUsecase(repository: repository).findAll(uid)
    }
}

extension ToDoListPresenter: ToDoListLoadComplateDelegate {
    
    func success(todoItems: [ToDo]) {
        viewInput?.setStatus(.Done)
        viewInput?.success(todoItems)
    }
    
    func failure(error: NSError) {
        viewInput?.setStatus(.Error)
        viewInput?.failure(error)
    }
}
```

ToDo登録用のPresenterクラスです。
登録処理は、Usecaseに委譲します。

```swift:ToDoAddPresenter.swift 
import UIKit

class ToDoAddPresenter: NSObject {
    
    let repository = ToDoRepositoryImpl()
    
    //MARK:- Public Method
    func addTodo(uid: String, todo: ToDo) {
        ToDoUsecase(repository: repository).add(uid, todo: todo)
    }
}
```

## 2. Domain層
### 2.1. Usecase
認証関連のUsecaseクラスです。
今回は、会員登録、ログオフを割愛しています。

```swift:AuthenticationUsecase.swift
class AuthenticationUsecase {
    
    var repository: AuthenticationRepository?
    
    convenience init(repository: AuthenticationRepository) {
        self.init()
        self.repository = repository
    }
    
    func login(mailAddress: String, password: String) {
        repository?.login(mailAddress, password: password)
    }
    
    func uid() -> String {
        return repository?.uid() ?? ""
    }
}
```

ToDo関連のUsecaseクラスです。
CRUDの操作をするメソッド群です。

```swift:ToDoUsecase.swift

import Foundation

class ToDoUsecase {
    
    var repository: ToDoRepository?
    
    convenience init(repository: ToDoRepository) {
        self.init()
        self.repository = repository
    }
        
    func add(userID: String, todo: ToDo) {
        repository?.add(userID, todo: todo)
    }
    
    func update(userID: String, todo: ToDo) {
        repository?.update(userID, todo: todo)
    }
    
    func findAll(userID: String) {
        repository?.findAll(userID)
    }
}
```

### 2.2. Repository
Authenticationに関するメソッド群を定義しています。
今回は、会員登録、ログオフを割愛しています。

```swift:AuthenticationRepository.swift
import Foundation

protocol AuthenticationRepository: class {
    func login(mailAddress: String, password: String)
    func uid() -> String
}
```

ToDoに対するCRUDの操作をするメソッド群を定義しています。

```swift:ToDoRepository.swift
import Foundation

protocol ToDoRepository: class {
    func add(userID: String, todo: ToDo)
    func update(userID: String, todo: ToDo)
    func findAll(userID: String)
}
```

### 2.3. Entity
ToDo情報のEntity構造体です。
タイトルと完了済みフラグ、Firebaseでデータを管理するためのキーを保持しています。

```swift:ToDo.swift
import Foundation

struct ToDo {
    var key = ""
    var title = ""
    var isDone = false
    
    init(key: String = "", title: String, isDone: Bool) {
        self.key = key
        self.title = title
        self.isDone = isDone
    }
}
```

## 3. Data層
### 3.1. Repository
FirebaseのAuthenticationを利用した実装クラスです。

```swift:AuthenticationRepositoryImpl.swift
import Foundation
import Firebase
import FirebaseDatabase

class AuthenticationRepositoryImpl: AuthenticationRepository {
    
    var loginDelegate: LoginComplateDelegate?
    
    func login(mailAddress: String, password: String) {
        
        FIRAuth.auth()?.signInWithEmail(mailAddress, password: password) {[weak self] (user, error) in
            
            if let error = error {
                self?.loginDelegate?.failure(error)
                return
            }
            
            if let user = user {
                self?.loginDelegate?.success(user.uid)
            }
        }
    }
    
    func uid() -> String {
        return FIRAuth.auth()?.currentUser?.uid ?? ""
    }
}
```

Firebaseにアクセスする実装クラスです。

```swift:ToDoRepositoryImpl
import Foundation
import Firebase
import FirebaseDatabase

class ToDoRepositoryImpl: ToDoRepository {
    
    var todoListDelegate: ToDoListLoadComplateDelegate?
    
    func add(userID: String, todo: ToDo) {
        
        let ref = FIRDatabase.database().reference()
        let key = ref.child("todolist").childByAutoId().key
        let childUpdates = ["/user-todolist/\(userID)/\(key)/": ["title": todo.title, "isDone" : todo.isDone]]
        ref.updateChildValues(childUpdates)
    }
    
    func update(userID: String, todo: ToDo) {
        let ref = FIRDatabase.database().reference()
        let childUpdates = ["/user-todolist/\(userID)/\(todo.key)/": ["title": todo.title, "isDone" : todo.isDone]]
        ref.updateChildValues(childUpdates)
    }
    
    func findAll(userID: String) {
        
        let ref = FIRDatabase.database().reference()
        ref.child("user-todolist").child(userID).observeSingleEventOfType(.Value, withBlock: {[weak self] (snapshot) in
            
            guard let delegate = self?.todoListDelegate else { return }
            
            var todoItems = [ToDo]()
            
            for item in snapshot.children {
                let child = item as! FIRDataSnapshot
                let dic = child.value as! NSDictionary
                
                todoItems.append(ToDo(key: child.key, title: dic["title"] as! String, isDone: dic["isDone"] as! Bool))
            }
            delegate.success(todoItems)
            
        }) {[weak self] (error) in
            
            guard let delegate = self?.todoListDelegate else { return }
            delegate.failure(error)
        }
    }
}
```

## 4. その他
Firebaseの初期設定

```swift:AppDelegate.swift
import UIKit
import Firebase

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
        FIRApp.configure()
        return true
    }
}
```

Storyboardアクセス補助用のExstension

```swift:UIStoryboard+ViewController.swift
import UIKit

extension UIStoryboard {
    static func getViewController<T: UIViewController>(storyboardName: String, identifier: String) -> T? {
        return UIStoryboard(name: storyboardName, bundle: nil).instantiateViewControllerWithIdentifier(identifier) as? T
    }
}
```

# まとめ
ToDoリストをクリーンアーキテクチャを使って実装してみました。
Data層以外は、Firebaseを利用していることを意識しない作りにしました。

## Firebase上のストレージ
ちなみに、Firebase上には、下記のよう管理されています。

<img width="440" alt="スクリーンショット 2016-08-06 18.14.33.png" src="https://qiita-image-store.s3.amazonaws.com/0/71694/34efe0c9-cc4e-e702-a3cf-c966dcab1252.png">

いずれもまだ勉強中の段階ですので、誤りやもっとよい方法がございましたら、
ご教授頂ければ幸いです。