Edited at

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

More than 1 year has passed since last update.


はじめに

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

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

ご教授頂ければ幸いです。