122
120

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-08-07

はじめに

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

方針

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

前提知識

Firebaseの導入方法
Firebaseによる認証方法
FirebaseによるRealtime Databaseの使い方

ゴール

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

(1)ログイン画面

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

スクリーンショット 2016-08-07 9.45.56.png

(2)ToDo一覧画面

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

スクリーンショット 2016-08-07 9.46.23.png

(3)ToDo登録画面

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

スクリーンショット 2016-08-07 9.46.31.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

スクリーンショット 2016-08-07 9.52.02.png

ToDoList.Storyboard

スクリーンショット 2016-08-07 11.37.29.png

ToDoAdd.Storyboard

スクリーンショット 2016-08-07 11.39.19.png

1.2. ViewController

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

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に委譲しています。
また合わせてテーブルビューの構築を行っています。

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に委譲しています。

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に戻します。

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に戻します。

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に委譲します。

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クラスです。
今回は、会員登録、ログオフを割愛しています。

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の操作をするメソッド群です。

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に関するメソッド群を定義しています。
今回は、会員登録、ログオフを割愛しています。

AuthenticationRepository.swift
import Foundation

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

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

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でデータを管理するためのキーを保持しています。

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を利用した実装クラスです。

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にアクセスする実装クラスです。

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の初期設定

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

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上には、下記のよう管理されています。

スクリーンショット 2016-08-06 18.14.33.png

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

122
120
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
122
120

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?