⚠️ こちらの記事は非推奨です。
こちらの記事をご参照ください。
Cloud Firestore + SwiftUI + Ballcapで始めるFirebaseアプリ開発
PringはBallcapの前進となるライブラリで現在はBallcapを推奨しています。
Pringで始める簡単Firebase App開発 for iOS
まだFirebaseで開発をしたことがない、開発者向けの記事です。アンドロイド開発者やWeb開発者もすぐに始められるように、なるべく細やかに作業を解説します。
準備
開発環境
- macOS High Sierra
- Xcode 9.4
- Swift 4
- iOS 11
Xcodeプロジェクトの作成からFirebaseの準備
新しいXcodeプロジェクトを作ろう
まずはXcodeを起動して、新しいプロジェクトを作りましょう。
ここでは、Master-Detail Appを選択しましょう。
Product Nameには、任意でプロジェクトの名前を入れてください。Firebase Sample
などで構いません。
Organization IdentifierからBundle Identifierが生成されるため非常に重要です。お持ちのドメインなどがあればそれを使いましょう。
このようになればOKです。せっかくここまでは出来ましたが一度Xcodeのプロジェクトを閉じて下さい。
CocoapodsでFirebaseのSDKをインストールしよう
ターミナルを使って、プロジェクトのディレクトリまで移動してください。
プロジェクトのディレクトリで次のコマンドを実行します。
pod init
Podfile
が生成されて入れば成功です。
もしエラーが出た人は、Cocoapodsをインストールしましょう。
Podfileを次のように編集しましょう。
platform :ios, '11.0'
target 'Firebase Sample' do
use_frameworks!
pod 'Pring'
end
Podfileを保存して、次のコマンドを実行します。
pod install
次のように出れば成功です。
プロジェクトのディレクトリもファイルが増えていると思います。
Firebase Sample.xcworkspace
というWorkspaceが出来ているのでこちらを開きましょう。
次のようにプロジェクトにPodsが追加されているのを確認して下さい。
これでFirebase SDKのインストール作業は終了です。
Firebaseの準備をしよう
FirebaseのSDKをインストールしましたが、これでFirebaseが使える訳ではありません。Firebaseにアクセスしてデータベースの設定をしましょう。
Firebaseに行って新しいプロジェクトを追加しましょう。
ここからは、既に素晴らしい記事が上がっていたのでこちらをご参考ください。
この作業を終えるとこんな感じにGoogleService-Info.plist
が追加されているはずです。
開発を始めよう
Xcodeで作成したプロジェクトの初期設定ではMain.storyboardを使ってViewControllerを起動するようになっていますが、今回はある理由から最初のViewControllerの起動はAppDelegateで行うことにします。
まず、プロジェクトのDeployment InfoのMain Interfaceを空にします。
次にAppDelegateを次のように編集します。
import UIKit
import Firebase
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
FirebaseApp.configure()
let storyboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let splitViewController: UISplitViewController = storyboard.instantiateInitialViewController() as! UISplitViewController
let navigationController = splitViewController.viewControllers[splitViewController.viewControllers.count-1] as! UINavigationController
navigationController.topViewController!.navigationItem.leftBarButtonItem = splitViewController.displayModeButtonItem
splitViewController.delegate = self
self.window = UIWindow(frame: UIScreen.main.bounds)
self.window?.rootViewController = splitViewController
self.window?.makeKeyAndVisible()
return true
}
// MARK: - Split view
func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController:UIViewController, onto primaryViewController:UIViewController) -> Bool {
guard let secondaryAsNavController = secondaryViewController as? UINavigationController else { return false }
guard let topAsDetailController = secondaryAsNavController.topViewController as? DetailViewController else { return false }
if topAsDetailController.detailItem == nil {
// Return true to indicate that we have handled the collapse by doing nothing; the secondary controller will be discarded.
return true
}
return false
}
}
ここで一度ビルドしてみましょう。
真っ白なMasterViewControllerが表示されれば成功です。
Note
今回XcodeからMain.storyboardを起動させなかった理由は、Firebaseの初期化のタイミングにあります。FirebaseApp.configure()
はapplication(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool
で記述しますが、Main.storyboardはそれより前にロードされるためMainViewControllerでFirebaseを利用できなくなってしまいます。
Firebaseに保存しよう
まずデータを保存するモデルを記述しましょう。
まずは、プロジェクトに新しくSwift Fileを追加します。
今回はItemというモデルを作ってみましょう。
そしてItemを次のように編集して下さい。
PringはCloud Firestoreのドキュメントをオブジェクトとして扱うことができるライブラリで、以下のようにItemを記述することができます。
import Pring
@objcMembers
class Item: Object {
dynamic var name: String?
}
それでは早速Item
をFirebaseに保存してみましょう。
MasterViewController
のfunc insertNewObject(_ sender: Any)
を次のように編集して下さい。
@objc
func insertNewObject(_ sender: Any) {
let item: Item = Item()
item.name = "hoge"
item.save()
}
では、Xcodeでビルドしてみましょう。
ではFirebaseのコンソールにアクセスして、CloudFirestoreの中に次のように出ていれば成功です。
もし出ていなければルールを確認して下さい。今はサンプルコード実行のため非常にゆるいセキュリティルールを適用していますが、これではプロダクトとして出せないので気をつけましょう。
Note
つい先日、Firebaseのセキュリティの緩さがエンジニア界隈で話題になりました。
この件に関しては、こちらを参考にして下さい。「Firebaseの脆弱性で1億件超漏洩」の真相とは?
MasterViewControllerとFirebaseを繋げよう
FirebaseにItem
を保存することができました。次にItemをMasterViewController
のTableViewに表示させてみましょう。
まず、MasterViewController
にもPringをimportします。
import UIKit
import Pring // PringをImport
次にDataSourceを準備しましょう。
var dataSource: DataSource<Item>?
viewDidLoad
の中で次を追記して下さい。ここでは深く説明しませんが、これで繋がります。
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
navigationItem.leftBarButtonItem = editButtonItem
let addButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(insertNewObject(_:)))
navigationItem.rightBarButtonItem = addButton
if let split = splitViewController {
let controllers = split.viewControllers
detailViewController = (controllers[controllers.count-1] as! UINavigationController).topViewController as? DetailViewController
}
// DataSourceを準備
self.dataSource = Item.query.dataSource()
.on({ [weak self] (snapshot, changes) in
guard let tableView: UITableView = 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()
}
TableViewのDelegateを次のように書き直して作業は完了です。
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.dataSource?.count ?? 0
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
let item: Item = self.dataSource![indexPath.row]
cell.textLabel!.text = item.name
return cell
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
self.dataSource!.removeDocument(at: indexPath.row)
} else if editingStyle == .insert {
// Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view.
}
}
MasterViewControllerを載せると次のようになります。
早速ビルドしてみましょう。
import UIKit
import Pring
class MasterViewController: UITableViewController {
var detailViewController: DetailViewController? = nil
var dataSource: DataSource<Item>?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
navigationItem.leftBarButtonItem = editButtonItem
let addButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(insertNewObject(_:)))
navigationItem.rightBarButtonItem = addButton
if let split = splitViewController {
let controllers = split.viewControllers
detailViewController = (controllers[controllers.count-1] as! UINavigationController).topViewController as? DetailViewController
}
self.dataSource = Item.query.dataSource()
.on({ [weak self] (snapshot, changes) in
guard let tableView: UITableView = 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()
}
override func viewWillAppear(_ animated: Bool) {
clearsSelectionOnViewWillAppear = splitViewController!.isCollapsed
super.viewWillAppear(animated)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
@objc
func insertNewObject(_ sender: Any) {
let item: Item = Item()
item.name = "hoge"
item.save()
}
// MARK: - Segues
// ここでは一旦コメントアウト
// override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// if segue.identifier == "showDetail" {
// if let indexPath = tableView.indexPathForSelectedRow {
// let object = objects[indexPath.row] as! NSDate
// let controller = (segue.destination as! UINavigationController).topViewController as! DetailViewController
// controller.detailItem = object
// controller.navigationItem.leftBarButtonItem = splitViewController?.displayModeButtonItem
// controller.navigationItem.leftItemsSupplementBackButton = true
// }
// }
// }
// MARK: - Table View
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.dataSource?.count ?? 0
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
let item: Item = self.dataSource![indexPath.row]
cell.textLabel!.text = item.name
return cell
}
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
// Return false if you do not want the specified item to be editable.
return true
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
self.dataSource!.removeDocument(at: indexPath.row)
} else if editingStyle == .insert {
// Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view.
}
}
}
次のようにHogeが出ましたか?そうすれば成功です。
Firebaseのリアルタイム性を体験しよう
さてさてここで、Firebaseのリアルタイム感を体験してみましょう。
右上の「+」ボタンを押してみましょう。Hogeが増えましたね?
Firebaseの分断耐性を体験しよう
次にMacのWiFiをOffにしてオフライン環境を作りましょう。そして同じく右上の「+」ボタンを押してみましょう。
シミュレーターのHogeが増えることが確認できたらFirebaseのコンソールにアクセスしてデータがまだ増えてないことを確認して下さい。(別の端末でみないとWiFi切れてますよ。)
データが増えてないことを確認したらWiFiを復活させてみましょう。WiFiを復活させて少し待つとコンソールにデータが反映されたことが確認できるはずです。
データの削除もしてみましょう。
左上の「Edit」ボタンを押すことでデータの削除をすることができます。削除した後にコンソールからデータが消えることを確認しましょう。
データを更新する
保存したデータを更新するために、まずDetailViewController
を修正して行きます。まずdetailItem
をItem
に変更して下さい。
var detailItem: Item? {
didSet {
// Update the view.
configureView()
}
}
func configureView() {
// Update the user interface for the detail item.
if let item = detailItem {
if let label = detailDescriptionLabel {
label.text = item.name
}
}
}
次にMasterViewController
も修正しましょう。コメントアウトしてたところを次の用に変更します。
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "showDetail" {
if let indexPath = tableView.indexPathForSelectedRow {
let object: Item = self.dataSource![indexPath.row]
let controller = (segue.destination as! UINavigationController).topViewController as! DetailViewController
controller.detailItem = object
controller.navigationItem.leftBarButtonItem = splitViewController?.displayModeButtonItem
controller.navigationItem.leftItemsSupplementBackButton = true
}
}
}
実行して見ましょう。次のようにhogeが表示されれば成功です。
次にStoryboardを修正して、TextField
とButton
を追加しましょう。これをDetailViewController
をIBOutlet, IBActionで繋いで下さい。
最終的にDetailViewController
は次のようになります。
import UIKit
import Pring
class DetailViewController: UIViewController {
@IBOutlet weak var detailDescriptionLabel: UILabel!
@IBOutlet weak var textField: UITextField!
@IBAction func updateAction(_ sender: Any) {
self.detailItem?.name = self.textField.text
self.detailItem?.update()
}
func configureView() {
// Update the user interface for the detail item.
if let item = detailItem {
if let label = detailDescriptionLabel {
label.text = item.name
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
configureView()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
var disposer: Disposer<Item>?
var detailItem: Item? {
didSet {
// Update the view.
configureView()
self.disposer = detailItem?.listen { [weak self] (item, error) in
if let error: Error = error {
print(error)
return
}
self?.configureView()
}
}
}
deinit {
self.disposer?.dispose()
}
}
では実行してみましょう。適当な文字を入れて更新ボタンを押して下さい。
TextLabelに入力した文字が表示されれば成功です。
ここで注目して頂きたいのが、listenの機能です。次のようにObjectに対してlisten
とするとリアルタイムでデータベースを監視することが可能です。非常に簡単ですね。
self.disposer = detailItem?.listen { [weak self] (item, error) in
if let error: Error = error {
print(error)
return
}
self?.configureView()
}
画像をアップロードする
サービスを作るためには、メディアをアップロードする必要があります。Pringでの写真アップロードの方法をご説明します。
Item
を次のように修正して下さい。PringではFile
という型を使って画像をアップロードすることが可能です。File
を配列にすることで複数画像をアップロードすることも可能ですが、今回は一枚だけアップロードします。
import Pring
@objcMembers
class Item: Object {
dynamic var name: String?
dynamic var image: File?
// 複数画像をアップロードしたい場合は配列にもOK
dynamic var images: [File] = []
}
Storyboardをさらに修正してDetailViewControllerにImageView
を追加してIBOutletでつなぎましょう。
UIImagePickerControllerで画像を選択するようにするので、次のようにDetailViewController
を修正して下さい。
viewDidLoad
でImageViewをタップするとUIImagePickerを呼び出せるようにします。
override func viewDidLoad() {
super.viewDidLoad()
configureView()
let tapGestureRecognizer: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didTapImageView))
self.imageView.addGestureRecognizer(tapGestureRecognizer)
self.imageView.isUserInteractionEnabled = true
}
@objc func didTapImageView() {
let imagePickerController: UIImagePickerController = UIImagePickerController()
imagePickerController.sourceType = .photoLibrary
imagePickerController.delegate = self
self.present(imagePickerController, animated: true, completion: nil)
}
次に選択した画像をアップロードします。画像のアップロードはitemにFileをセットしてupdate
を実行するだけです。
extension DetailViewController: UINavigationControllerDelegate, UIImagePickerControllerDelegate {
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
guard let image: UIImage = info[UIImagePickerControllerOriginalImage] as? UIImage else { return }
let data: Data = UIImageJPEGRepresentation(image, 0.5)!
self.detailItem?.image = File(data: data)
self.uploadTasks = self.detailItem?.update()
picker.dismiss(animated: true, completion: nil)
}
}
アップロード中のプログレスはuploadTasks
の中に含まれるStorageUploadTask
で取得することができます。
var uploadTasks: [String: StorageUploadTask]? {
didSet {
let uploadTask: StorageUploadTask = uploadTasks![uploadTasks!.keys.first!]!
uploadTask.observe(.progress) { (snapshot) in
print(snapshot.progress)
}
}
}
configureView
も少し修正しましょう。
var donwloadTask: StorageDownloadTask?
func configureView() {
// Update the user interface for the detail item.
if let item = detailItem {
if let label = detailDescriptionLabel {
label.text = item.name
}
donwloadTask = item.image?.getData(completion: { [weak self](data, error) in
if let error = error {
print(error)
return
}
self?.imageView.image = UIImage(data: data!)
})
}
}
最終的にDetailViewController
は次のようになります。
import UIKit
import Firebase
import Pring
class DetailViewController: UIViewController {
@IBOutlet weak var detailDescriptionLabel: UILabel!
@IBOutlet weak var imageView: UIImageView!
@IBOutlet weak var textField: UITextField!
@IBAction func updateAction(_ sender: Any) {
self.detailItem?.name = self.textField.text
self.detailItem?.update()
}
var uploadTasks: [String: StorageUploadTask]? {
didSet {
let uploadTask: StorageUploadTask = uploadTasks![uploadTasks!.keys.first!]!
uploadTask.observe(.progress) { (snapshot) in
print(snapshot.progress)
}
}
}
var donwloadTask: StorageDownloadTask?
func configureView() {
// Update the user interface for the detail item.
if let item = detailItem {
if let label = detailDescriptionLabel {
label.text = item.name
}
donwloadTask = item.image?.getData(completion: { [weak self](data, error) in
if let error = error {
print(error)
return
}
self?.imageView.image = UIImage(data: data!)
})
}
}
override func viewDidLoad() {
super.viewDidLoad()
configureView()
let tapGestureRecognizer: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didTapImageView))
self.imageView.addGestureRecognizer(tapGestureRecognizer)
self.imageView.isUserInteractionEnabled = true
}
@objc func didTapImageView() {
let imagePickerController: UIImagePickerController = UIImagePickerController()
imagePickerController.sourceType = .photoLibrary
imagePickerController.delegate = self
self.present(imagePickerController, animated: true, completion: nil)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
var disposer: Disposer<Item>?
var detailItem: Item? {
didSet {
// Update the view.
configureView()
self.disposer = detailItem?.listen { [weak self] (item, error) in
if let error: Error = error {
print(error)
return
}
self?.configureView()
}
}
}
deinit {
self.disposer?.dispose()
}
}
extension DetailViewController: UINavigationControllerDelegate, UIImagePickerControllerDelegate {
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
guard let image: UIImage = info[UIImagePickerControllerOriginalImage] as? UIImage else { return }
let data: Data = UIImageJPEGRepresentation(image, 0.5)!
self.detailItem?.image = File(data: data)
self.uploadTasks = self.detailItem?.update()
picker.dismiss(animated: true, completion: nil)
}
}
では実行してみましょう。
ImageViewをタップして画像を選択すれば数秒で画像がアップロードされるはずです。そしてImageViewに画像が表示されれば成功です。
また、プログレスがprint
で表示されいるので見てみましょう。
このようにPringはApp開発に置いて必要になってくる機能を助けてくれる強力な機能を提供しています。