以下の記事でFirebaseのセットアップからプッシュ通知を受け取るところまで実装したので、次はFirestoreにデータを書き込み、それをトリガーにしたCloud Functions経由でプッシュ通知を送信するように実装していく。
Firestoreをセットアップする
Podfile
に pod 'Firebase/Firestore'
/ pod 'Pring'
/ pod 'Firebase/Auth'
と追加して pod install
を実行する。Xcodeのバージョンが9.2以下だとうまく動かなかったので、もしXcodeのバージョンが低い場合はアップグレードする必要がある。Pringを利用すると直感的にモデルを扱えるので今回はこれを利用する。Firestoreにデータを書き込むためには、認証が必要なので Firebase/Auth
も利用する(Firestoreのセキュリティルールをゆるくすれば必要はないがせっかくなので認証も実装する)。
Pringのリンクはこちら。わかりやすいので個人的には気に入っている。
ただ、使い方が若干わかりにくいので、この記事を参考にしていただければ嬉しい。
続いてFirebaseのコンソールに行って、Databaseの初期化をする。
Databaseのルールを開き、下の画像のように編集する。このセキュリティルールが甘いせいでユーザの情報が流出する事件が先日起こったのでよく注意しよう。
Firebase Authenticationも利用するので、初期設定をしていく。
匿名の欄を開いて有効化する。ここの設定を忘れるとうまくいかないのだが、忘れがちなので気を付けるべきポイント。
モデルを実装していく
新しく Model.swift
を作成して以下のようにObjectを継承したUserとBookを宣言する。本来ならそれぞれ User.swift
・ Book.swift
のように分割する方が綺麗かもしれないがここではまとめて書いておく。 User
については、匿名ログイン時に発行されるIDを使ってデータを生成し、すでに User
が生成済みであれば何もしないようにしている。 User
と Book
の関係は、 User has many Books
で、 NestedCollection を利用するとデータを取得する時に後々効いてくる。
// Model.swift import Foundation import Pring @objcMembers class User: Object { dynamic var name: String? dynamic var fcmToken: String? dynamic var books: NestedCollection<Book> = [] static func anonymousLogin(_ completionHandler: @escaping ((User?) -> Void)) { Auth.auth().signInAnonymously { (auth, error) in if let error = error { print(error.localizedDescription) completionHandler(nil) return } guard let currentUser = Auth.auth().currentUser else { completionHandler(nil) return } User.get(currentUser.uid) { (user, _) in if let user = user { print("Success login of an existing user") completionHandler(user) } else { let u = User(id: auth!.user.uid) u.name = "Alice" u.save() { ref, error in print("Success login of a new user") completionHandler(u) } } } } } } @objcMembers class Book: Object { dynamic var name: String? }
ViewController.swift
にログインの実装を加える。
// ViewController.swift import UIKit import UserNotifications class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound] UNUserNotificationCenter.current().requestAuthorization(options: authOptions) { _, _ in print("push permission finished") } // ここだけ追加 User.anonymousLogin() { user in print(user) } } }
この時点でXcodeで実行するとFirebase Authにてログイン済みのUserが追加され、Firestoreにも同じIDを持ったUserのデータが生成されている。認証周りとデータの書き込みがうまく行っていることが確認できる。
データの更新をトリガーにプッシュ通知を送信する
FCMトークンの扱い
まず AppDelegate.swift
で受け取れるFCMトークンをUserオブジェクトと紐づける。ログイン処理が完了した後にFCMトークンをFirestoreの該当するUserに記録したいので UserDefaults
を利用する。 AppDelegate.swift
内の、FCMトークンを受け取った場所で以下のように実装する。
// AppDelegate.swift func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String) { // FCMトークンを記録しておく UserDefaults.standard.set(fcmToken, forKey: "FCM_TOKEN") UserDefaults.standard.synchronize() print("Firebase registration token: \(fcmToken)") }
次に ViewController.swift
で匿名ログインをしているが、ログイン完了時にUserオブジェクトの fcmToken
に UserDefaults
に記録しておいた値をセットして update
をする。
// ViewController.swift import UIKit import UserNotifications class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound] UNUserNotificationCenter.current().requestAuthorization(options: authOptions) { _, _ in print("push permission finished") } User.anonymousLogin() { user in // 匿名ログイン完了時にfcmTokenをセットしてupdateする if let user = user { user.fcmToken = UserDefaults.standard.string(forKey: "FCM_TOKEN") user.update() } } } }
この状態で実行すると以下の画像のようにFCMトークンがセットされているはず。
Firestoreに新しいデータを書き込む
まず、Firestoreにデータを書き込む。 ViewController.swift
を少し変更する。
// ViewController.swift import UIKit import UserNotifications class ViewController: UIViewController { private var user: User? override func viewDidLoad() { super.viewDidLoad() let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound] UNUserNotificationCenter.current().requestAuthorization(options: authOptions) { _, _ in print("push permission finished") } User.anonymousLogin() { user in self.user = user if let user = user { user.fcmToken = UserDefaults.standard.string(forKey: "FCM_TOKEN") user.update() } } // ここを追加する setButton() } private func setButton() { let button = UIButton(frame: CGRect(x: 0, y: 50, width: view.frame.width, height: view.frame.height / 6)) button.backgroundColor = .black button.setTitle("Add Book", for: .normal) button.setTitleColor(.white, for: .normal) button.addTarget(self, action: #selector(addBook), for: .touchUpInside) view.addSubview(button) } @objc private func addBook(_ sender: UIButton){ let book = Book() book.name = "Alice in Wonderland" user?.books.insert(book) user?.update() } }
Xcodeで起動すると以下のようにボタンがセットされている。これをタップすると、FirestoreのUserの中にBookが記録される。
Firebaseのコンソールで確認すると以下の画像の通り。
Cloud Functionsを使ってFirestoreの更新を監視する
サーバレスで実行されるCloud Functionsを実装していく。これはAWSのLambdaに相当する。GCPのコンソール側から作成することもできるが、Firebaseのコンソールから進めていくほうがずっと楽である。
以下のコマンドでFirebaseのCLIをインストールする。
$ npm install -g firebase-tools
続いて、Firestoreの更新があった時に、それをトリガーに実行されるコードを書いていく。Xcodeのプロジェクトとは異なるディレクトリに移動して、 firebase init
を実行する。 Functions
をスペースで選択し、次へ進む。
いくつか質問が聞かれるがここでは以下のように進めた。
=== Project Setup ? Select a default Firebase project for this directory: FirebaseSample (fir-sample-94ad3) === Functions Setup ? What language would you like to use to write Cloud Functions? JavaScript ? Do you want to use ESLint to catch probable bugs and enforce style? No ✔ Wrote functions/package.json ✔ Wrote functions/index.js ? Do you want to install dependencies with npm now? Yes ✔ Firebase initialization complete!
functions
ディレクトリの中にある index.js
を編集していく。
// functions/index.js const functions = require('firebase-functions') const admin = require('firebase-admin') admin.initializeApp() const firestore = admin.firestore() const pushMessage = (fcmToken, bookName) => ({ notification: { title: '保存が完了しました', body: `「${bookName}」の保存が完了しました🙌`, }, data: { hoge: 'fuga', // 任意のデータを送れる }, token: fcmToken, }) exports.saveBook = functions.firestore .document('version/1/user/{userID}/books/{bookID}') .onCreate((snapshot, context) => { // 以下のようにすればbookの中身が取れる. const book = snapshot.data() // 以下のようにすればuserIDやbookIDが取れる. const userID = context.params.userID const userRef = firestore.doc(`version/1/user/${userID}`) userRef.get().then((user) => { const userData = user.data() admin.messaging().send(pushMessage(userData.fcmToken, book.name)) .then((response) => { console.log('Successfully sent message:', response) }) .catch((e) => { console.log('Error sending message:', e) }) }).catch((e) => console.log(e)) })
実装したら、 firebase deploy
をターミナル側で実行してデプロイする。
以下のようにデプロイされているのが確認できればOK。
最後にiPhone実機でAdd Bookボタンをタップすると、数秒後にプッシュ通知が届く!!
うまくプッシュ通知が届かない時は、FirebaseのコンソールのFunctionsのタブでログを確認しよう。
ついでに保存されているBookの一覧をTableViewで表示してみる。
せっかくなので、Firestoreに保存されているBookの一覧を表示してみる。 ViewController.swift
に UITableView
を追加して、FirestoreのUser/Booksの監視を行うように実装する。
// ViewController.swift import UIKit import UserNotifications import Pring class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { private var user: User? private var dataSource: DataSource<Book>? // TableViewに渡すDataSource private var tableView: UITableView! override func viewDidLoad() { super.viewDidLoad() let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound] UNUserNotificationCenter.current().requestAuthorization(options: authOptions) { _, _ in print("push permission finished") } setButton() setTableView() User.anonymousLogin() { [weak self] user in self?.user = user if let user = user { user.fcmToken = UserDefaults.standard.string(forKey: "FCM_TOKEN") user.update() let options = Options() let sortDescriptor = NSSortDescriptor(key: "createdAt", ascending: false) options.sortDescirptors = [sortDescriptor] // 以下のように書くと、Firestoreの更新があるとTableViewの更新もかけるようになる. self?.dataSource = user.books.order(by: \Book.createdAt).dataSource(options: options) .on() { (snapshot, changes) in guard let tableView = self?.tableView else { return } switch changes { case .initial: tableView.reloadData() case .update(let deletions, let insertions, let modifications): tableView.beginUpdates() tableView.insertRows(at: insertions.map { IndexPath(row: $0, section: 0) }, with: .automatic) tableView.deleteRows(at: deletions.map { IndexPath(row: $0, section: 0) }, with: .automatic) tableView.reloadRows(at: modifications.map { IndexPath(row: $0, section: 0) }, with: .automatic) tableView.endUpdates() case .error(let error): print(error) } }.listen() } } } private func setButton() { let button = UIButton(frame: CGRect(x: 0, y: 50, width: view.frame.width, height: view.frame.height / 6)) button.backgroundColor = .black button.setTitle("Add Book", for: .normal) button.setTitleColor(.white, for: .normal) button.addTarget(self, action: #selector(addBook), for: .touchUpInside) view.addSubview(button) } @objc private func addBook(_ sender: UIButton){ let book = Book() book.name = "Alice in Wonderland" user?.books.insert(book) user?.update() } private func setTableView() { let f = view.frame // Add Bookボタンの下に配置する tableView = UITableView(frame: CGRect(x: 0, y: 50 + f.height / 6, width: f.width, height: f.height * 5 / 6 - 50), style: .plain) tableView.delegate = self tableView.dataSource = self tableView.tableFooterView = UIView() tableView.register(UITableViewCell.self, forCellReuseIdentifier: "BookCell") view.addSubview(tableView) } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return dataSource?.count ?? 0 } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "BookCell", for: indexPath) cell.textLabel?.text = dataSource?[indexPath.item].name return cell } }
完成したアプリがこちら
FirebaseSampleのコードはこちら
FirebaseSampleFunctionsのコードはこちら