はじめに
Firebaseを勉強したので、簡単なToDoリストを作ってみます。
方針
・設計手法は、クリーンアーキテクチャを採用
・データの管理は、FirebaseのRealtime Databaseを採用
前提知識
・Firebaseの導入方法
・Firebaseによる認証方法
・FirebaseによるRealtime Databaseの使い方
ゴール
ユーザごとにToDoリストを管理するアプリを作ります。
画面構成は、ログイン画面、ToDo一覧画面と、ToDo登録画面です。
(会員登録機能、ログアウト機能については、割愛します。)
(1)ログイン画面
・メールアドレス、パスワードを入力し、ログインボタンを押下すると、ToDo一覧画面へ遷移する
(2)ToDo一覧画面
・ToDo一覧を表示する
・該当行をタップするとToDoを完了済みとする(取り消し線で消す)
・プラスボタンを押下すると、ToDo登録画面へ遷移する
(3)ToDo登録画面
・タイトルを入力し、登録ボタンを押下すると、ToDo一覧画面に戻る
・バックボタンを押下すると、ToDo一覧画面に戻る
クラスの構成
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
ToDoList.Storyboard
ToDoAdd.Storyboard
1.2. ViewController
ログイン画面のViewControllerクラスです。
基本的に処理はすべてPresenterに委譲します。
(バリデーションチェック等のエラー処理は割愛しています)
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に委譲しています。
また合わせてテーブルビューの構築を行っています。
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に委譲しています。
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に戻します。
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に戻します。
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に委譲します。
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クラスです。
今回は、会員登録、ログオフを割愛しています。
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の操作をするメソッド群です。
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に関するメソッド群を定義しています。
今回は、会員登録、ログオフを割愛しています。
import Foundation
protocol AuthenticationRepository: class {
func login(mailAddress: String, password: String)
func uid() -> String
}
ToDoに対するCRUDの操作をするメソッド群を定義しています。
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でデータを管理するためのキーを保持しています。
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を利用した実装クラスです。
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にアクセスする実装クラスです。
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の初期設定
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
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上には、下記のよう管理されています。
いずれもまだ勉強中の段階ですので、誤りやもっとよい方法がございましたら、
ご教授頂ければ幸いです。