1. はじめに
もうすぐ年の瀬やクリスマスが近づいて、平素の業務でもプライベートでも忙しくなる時期にはなりますが皆様お疲れ様です!
最近になってFirebaseに関して色々調べたり、簡単なサンプル作成に取り組んでいました。
Firebaseに関しては簡単なサンプルを動かしてみたり等の部分は試してみたことはありますが、実際に作成していくと、単純なDatabaseへの読み書きであれば非常に簡単に行うことができるのですが、Storageへ画像ファイルのアップロードも一緒に行いたい場合の処理を書いた際に、結構ハマってしまった&その際に「Salada」というFirebaseデータを扱いやすくするライブラリを使用して簡易的なサンプルを作成してみましたのでその知見と作成したサンプルの紹介と解説をしたいと思います。
※試しながらの実装になっている部分もあってごくごくシンプルなサンプルになっていますので、間違いやご指摘点等あればご遠慮なくお申し付け頂ければ幸いに思います。
2. 一番ハマった部分は複数画像のFirebase Storageへのアップロード処理
FirebaseのStorageへのアップロードに関しては、取り組んでいて一番結構ハマッてしまった部分でした。(結果的には自分の処理の書き方が悪かったのでそうなってしまいました。。。)
最初はbase64でのEncode/Decodeを利用してStorageではなく、Databaseの中で保存する処理を試みましたが、
- データの取得or保存時に性能劣化が激しく速度がかなり遅くなった
という問題が生じたので、この方法は使用しないことにしました。
また複数の画像をアップロードする際においては、UploadToFirebaseStorage
メソッドにてFirebaseのストレージへ画像をアップロードするメソッドのコールバックとDispatchQueue
を用いての並列処理を組み合わせて処理を行う形をとると良いかと思います。
(この形でよいのかはFirebaseにまだそれほど慣れているわけではないので、ちょっと自信がないのですが、下記にコードでの簡単なイメージを載せておきます。ただこちらはあくまでこのコードはイメージなのでその点はご容赦ください。)
let queue = DispatchQueue(label: "任意の名前")
queue.async(group: group) {
UploadToFirebaseStorage(photoIdentifier: photoIdentifier, photoData: photoData!, block: { (ref, error) in
if let error: Error = error {
print(error)
return
}
//TODO: groupを抜ける等の処理
})
}
参考資料:[Swift 3] Swift 3時代のGCDの基本的な使い方
実際にアップロード処理を自前で構築するような場合においては、画像のサイズや通信状態(速度制限がかかった状態での大きいサイズの複数画像のアップロードはかなり遅い)等も考慮をしなければならない部分なので、アプリから画像投稿をする際等は考慮しなければいけない部分かなと個人的に感じました。
また、このような並列処理を行うような場合には、並列処理をうまく行うためのライブラリの導入に関しても検討をしてみても良いかと思います。
3. 今回作成した簡易的なサンプルと使用ライブラリ「Salada」の紹介
お試しサンプルを作成して自分なりにではありますが試行錯誤をしながら実装していく過程で、個人的にではありますが下記のような要望も生じてくるようになりました。
- データの項目が多くなった際に1つのクラスに項目と型をまとめておきたい
- 画像のアップロード処理に関する部分が考慮されているWrapperクラスが用意されているとうれしい
- 普段使い慣れているActiveRecordやRealmのような感覚でデータの取得や保存をしたい
上記の点を考慮し今回は、Firebaseを扱いやすくするライブラリである「Salada」を利用しました。
こちらのライブラリはFirebaseを扱いやすくしてくれるものになります。特にFirebase Storageへ複数の画像(ファイル)アップロードの際の並列処理の考慮がすでにされていたことや、一意なIDを持たせてその中にデータを持たせる設計がすでにされた状態でデータが格納されることが考慮されていた部分は本当に助かりました。
またFirebaseのデータ設計に関する考え方や、データの持ち方に関するポイントに関しては下記のドキュメントが非常に参考になりました。
今まではサーバーサイドのエンジニアだったこともあり、MySQL等のRDBの扱いに関しては慣れてはいたものの、Firebaseは何せ初めてな部分があったので、私自身もまだまだ不慣れな部分がありますが、しっかりと深堀りしていきたい点なので今後とも追っかけていく次第です。
4. 「Salada」の導入と使用した際に作成されるデータ持ち方及びデータ処理ロジックに関するまとめ
今回のサンプルに関しては、まずは簡単な設計でかつ画面数もさほど多くない所から試してみようと思ったこともあり、画像アップロード機能が付いたToDoリストの追加と削除ができるサンプルを作成しました。
サンプルの全体的な動き:
※こちらのサンプルはPullRequestやIssue等はお気軽にどうぞ!
★4-1. サンプルの概要について
本サンプルの画面構成に関しては、下記のように3画面だけの至ってシンプルな構成になります。
- 登録したToDoの一覧を表示する画面(ActionSheetで詳細表示orデータ削除を選択する)
- ToDoに関するデータを登録する画面(内訳は「ToDo名」・「具体的にすること」・「画像は1枚~3枚」)
- ToDoの詳細表示をする画面(登録されたデータに紐づくFirebase Storage内の画像を表示する)
※1. 本サンプルでは、Firebaseを導入する際に必要なGoogleService-Info.plist
は除外していますので、サンプルを実行する際に関しましては、適宜ご自身で準備をして頂ければ幸いです。
※2. データの読み込み時とデータ登録時のローディング表示に関しては、SVProgressHUDを使用しています。
環境やバージョンについて:
- Xcode8.2
- Swift3.0
- MacOS Sierra (Ver10.12.2)
★4-2. Firebaseの導入とSaladaの導入について
iOS版のFirebaseの導入に関しては、下記の公式ドキュメントや下記のQiitaの記事で詳しく解説されていましたのでこちらを参考にして、まずはFirebaseを自分のプロジェクト内に導入します。
今回使用するのは、Firebaseの機能の中でFirebase/Database
とFirebase/Storage
の2つになるので、今回のサンプルのPodfileは下記のようになります。
platform :ios, '9.0'
swift_version = '3.0'
target 'SaladaSample' do
use_frameworks!
pod 'Firebase'
pod 'Firebase/Storage'
pod 'Firebase/Database'
pod 'SVProgressHUD', :git => 'https://github.com/SVProgressHUD/SVProgressHUD.git'
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['SWIFT_VERSION'] = '3.0'
end
end
end
end
また、Firebaseを扱いやすくするライブラリのSaladaの導入手順に関しては、SaladaのREADMEにも記載と重複にはなってしまいますが、
- まずはこのプロジェクトのファイルをダウンロードもしくはクローンする
- プロジェクト内にある
Salada.swift
ファイルを自分のプロジェクトに導入する
で使用するための準備は完了となります。
注意:
本サンプルではread/writeの権限を便宜上trueにした状態で実装しましたが、実際にご利用される際はDatabase及びStorageのルールに関しては、FirebaseAuthを利用して認証による制限を行うようして下さい。
参考:
DatabaseのRule:
# データベースへの読み込み/書き込みに認証が必要な状態(デフォルト)
{
"rules": {
".read": "auth != null",
".write": "auth != null"
}
}
# 誰でもデータベースへの読み込み/書き込みができる状態(セキュリティの関係上よろしくない)
{
"rules": {
".read": true,
".write": true
}
}
StorageのRule:
# 誰でもストレージへの読み込みはできるが、書き込みは認証を要する(デフォルト)
service firebase.storage {
match /b/<your-firbase-storage-bucket>/o {
match /{allPaths=**} {
allow read, write: if request.auth != null;
}
}
}
# 誰でもストレージへの読み込み/書き込みができる状態(セキュリティの関係上よろしくない)
service firebase.storage {
match /b/<your-firbase-storage-bucket>/o {
match /{allPaths=**} {
allow read, write
}
}
}
★4-3. 今回登録されるデータに関する設計とFirebase内のスキーマについて
Saladaを使用してデータの取得や読み書きを行うためには、はじめにIngredient
クラスを継承したTodolist
クラス(Todolist.swift)を作成する必要があります。
このクラスの中にはtypealias
の設定とFirebaseのDatabase内で保持する値のカラム名と型をそれぞれ設定しています。
import Foundation
class Todolist: Ingredient {
//typealiasの設定(必須)
typealias Tsp = Todolist
//登録対象のカラム名と型を定義する
dynamic var title: String?
dynamic var detail: String?
dynamic var progress: String?
dynamic var photo_count: String?
dynamic var image1: File?
dynamic var image2: File?
dynamic var image3: File?
}
上記のimage1〜3
に関しては、File?型になっていますが、こちらはStorageにアップロードするファイルを格納するための設定になります。
このようにTypealiasとFirebaseに保持したいデータに関する設定を行った上で、Saladaで提供されているメソッドを利用して、データの登録を行った際には、FirebaseのDatabaseとStorageには下記のような形で値やファイルが保持されます。
Database内のデータの持ち方:
Storage内のデータの持ち方:
このデータの持ち方を見ると、__(「v1 → todolist → 一意なID(FirebaseのAutoId) → Todolistクラスで設定したカラムに対応する値」)というような形で値を保持しています。またこの「v1」__に関してはこのデータのバージョン番号になるので、マイグレーションを行う場合の考慮がなされていることがわかります。
★4-4. 本サンプルのデータ登録部分の実装におけるポイントまとめ
データの登録部分に関しては、前述したIngredient
クラスを継承したTodolist
クラス(Todolist.swift)で作成したクラスのインスタンスを作成し、クラス内で設定したプロパティに対して、テキストフィールドに入力された値やフォトライブラリから取得した画像を格納しているUICollectionViewから選択した画像をそれぞれセットします。
今回での画像データの取り扱いに関しては、複数の画像アップロードに対応するためにPhotos.framework
を活用して取得してきたものを利用し、PHAsset
型のデータをconvertAssetOriginPhoto(asset: PHAsset) -> UIImage
メソッドでUIImage型に変換した上で、JPG型に変換を行い、さらにこのデータをFile型に変換して、該当のTodolist
クラスで定義したプロパティに代入して対応をしています。
※ UIImageJPEGRepresentation
メソッドの第2引数の値に関しては、今回のサンプルでは思いっきり値を小さく(0.1)としています。
各プロパティへの追加したら最後にsave()
メソッドを実行してFirebaseのDatabaseへデータの保存とStorageへのファイルのアップロードを行います。
このsave()
メソッドに関する概要をざっくりとまとめると、
- まずは新規の追加か否か(一意のIDがあるかで判断)し、ない場合は渡された値をDatabaseへ新規追加をする(登録日や更新日の付与等も含む)。一意のIDがある場合はデータの上書きをFirebaseの
runTransactionBlock
メソッド内で行う。 - 上記の処理が完了したら、一意のIDに紐づく値を
observeSingleEvent
メソッドで値を取得して、File型のデータがあればsaveFiles
メソッドを実行してStorageへのファイルのアップロード処理を行う
という形になっています。またこのsaveFiles
メソッドに関しては、並列処理を用いてのファイルアップロードの処理が考慮されています。
/**
Save the new Object to Firebase. Save will fail in the off-line.
- parameter completion: If successful reference will return. An error will return if it fails.
*/
public func save(_ completion: ((FIRDatabaseReference?, Error?) -> Void)?) {
if self.id == self.tmpID || self.id == self._id {
var value: [String: Any] = self.value
let timestamp: AnyObject = FIRServerValue.timestamp() as AnyObject
value["_createdAt"] = timestamp
value["_updatedAt"] = timestamp
var ref: FIRDatabaseReference
if let id: String = self._id {
ref = type(of: self).databaseRef.child(id)
} else {
ref = type(of: self).databaseRef.childByAutoId()
}
ref.runTransactionBlock({ (data) -> FIRTransactionResult in
if data.value != nil {
data.value = value
return .success(withValue: data)
}
return .success(withValue: data)
}, andCompletionBlock: { (error, committed, snapshot) in
type(of: self).databaseRef.child(ref.key).observeSingleEvent(of: .value, with: { (snapshot) in
self.snapshot = snapshot
// File save
self.saveFiles(block: { (error) in
completion?(ref, error as Error?)
})
})
}, withLocalEvents: false)
} else {
let error: IngredientError = IngredientError(kind: .invalidId, description: "It has been saved with an invalid ID.")
completion?(nil, error)
}
}
ここまでがSaladaを用いた、本サンプルでのFirebaseへのデータの登録処理の大まかな流れになります。
そして、下記が解説した部分に関する処理の全体になります。
//データの追加アクション
@IBAction func addDataAction(_ sender: UIButton) {
//バリデーションを行う
if ((todoTitle.text?.isEmpty)! || (todoDetail.text?.isEmpty)! || photoListDictionary.count == 0) {
・・・(省略)・・・
//OK:データを1件Firebaseにセーブする
} else {
//ボタンを押せなくしてインジケーターを表示する
addButton.isEnabled = false
wrappedView.alpha = 0.35
wrappedView.isHidden = false
SVProgressHUD.show(withStatus: "データ登録処理中...")
//PHAssetから取得したデータを(1)UIImage型→(2)Data型→(3)File型の順に変換してDictonary(saveImageData)に詰め直す
var saveImageData: [Int : File] = [:]
var newIndex = 1
for (_, value) in photoListDictionary {
let targetImage = convertAssetOriginPhoto(asset: value)
let photoData: Data = UIImageJPEGRepresentation(targetImage, 0.1)!
let photoIdentifier = String(format: "%02d", newIndex) + ".jpg"
let thumbnail: File = File(name: photoIdentifier, data: photoData)
saveImageData[newIndex] = thumbnail
newIndex += 1
}
//Todolistクラスのインスタンスを作成してそれぞれのプロパティに対応する値を入れる
let todolist: Todolist = Todolist()
todolist.title = todoTitle.text
todolist.detail = todoDetail.text
todolist.image1 = saveImageData[1] ?? nil
todolist.image2 = saveImageData[2] ?? nil
todolist.image3 = saveImageData[3] ?? nil
todolist.progress = "これから着手"
todolist.photo_count = String(saveImageData.count)
//saveメソッドを実行して、正常処理の終了時のコールバックを記載する
todolist.save({ (ref, error) in
if ref != nil {
//プログレスバーを非表示にする
SVProgressHUD.dismiss()
self.wrappedView.alpha = 0
self.wrappedView.isHidden = true
//登録されたアラートを表示してOKを押すと戻る
let correctAlert = UIAlertController(
title: "完了",
message: "入力データが登録されました。",
preferredStyle: UIAlertControllerStyle.alert
)
correctAlert.addAction(
UIAlertAction(
title: "OK",
style: UIAlertActionStyle.default,
handler: { (action: UIAlertAction!) in
self.dismiss(animated: true, completion: nil)
}
)
)
self.present(correctAlert, animated: true, completion: nil)
}
})
}
}
補足になりますが、このサンプルで使用するアップロード対象の画像選択に関する部分につきましては、複数の画像を選択可能にするために UICollectionViewとPhotos.frameworkを組み合わせた入力インターフェースを下記の記事を参考にして作成しました。
(今回はセルのタップを行うと一時的に選択データを格納する変数var photoListDictionary: [Int : PHAsset] = [:]
へ格納するような処理にしています)
また、「画像を同期する」ボタンを押下すると現在選択中の画像をすべてクリアし、再度フォトライブラリ内の画像を読み込み直す処理を実行するようにしています。
※1. UICollectionViewとPHAssetを組み合わせた表示の方法に関しては、詳細な実装の解説は今回は割愛しますが、近日中にまとめようと思います。
※2. FirebaseのobserveSingleEvent
メソッド並びにrunTransactionBlock
メソッドに関する説明は公式ドキュメントを参照して見てください。
★4-5. 本サンプルのデータ表示部分の実装におけるポイントまとめ
UITableViewの一覧に表示する部分に関しては、下記のようにSalada.swift内で定義されているobserveSingle
メソッドを利用して該当のクラスのデータを全件取得する処理を行います。
データの取得が完了したらそのデータを新しい順に並べ替えて、データの表示を行うという形になっています。
//※ Todolist.observeSingle内ではFirebaseのobserveSingleEventメソッドが実行されて対応するクラスのデータ取得を行なっている
fileprivate func loadTodoData() {
SVProgressHUD.show(withStatus: "データ取得中...")
//データの取得を行う
Todolist.observeSingle(.value, block: { todos in
self.todoList.removeAll()
todos.forEach({ (todo) in
self.todoList.append(todo)
})
SVProgressHUD.dismiss()
self.todoList.reverse()
self.listTableView.reloadData()
})
}
この処理のポイントになる点としては、Salada.swift
内のobserveSingle
メソッドでは、Todolist.swift
内で設定したtypealiasの値を元にして取得する対象のデータの格納場所を判別し、このクラス内でFirebaseのobserveSingle
メソッドを利用してデータを取得している点になるかと思います。
また、取得したデータに関してもtodolist.title
のような形で値を取得できるのでとても便利に感じています。
/**
A function that gets all data from DB whose name is model.
*/
public static func observeSingle(_ eventType: FIRDataEventType, block: @escaping ([Tsp]) -> Void) {
self.databaseRef.observeSingleEvent(of: eventType, with: { (snapshot) in
if snapshot.exists() {
var children: [Tsp] = []
snapshot.children.forEach({ (snapshot) in
if let snapshot: FIRDataSnapshot = snapshot as? FIRDataSnapshot {
if let tsp: Tsp = Tsp(snapshot: snapshot) {
children.append(tsp)
}
}
})
block(children)
} else {
block([])
}
})
}
また、詳細で画像を表示する部分に関しても、Salada.swift
内で、Firebaseで提供されているメモリ内のNSDataオブジェクトにファイルをダウンロードするdataWithMaxSize
メソッドを拡張したものが用意されているので、UIScrollViewでの表示部分ではこのメソッドを利用しています。
このようにFirebaseのデータに関しても、取得したデータを扱いやすくするような考慮であったり、一意なIDによるデータの管理や登録日・更新日の考慮等も含めた細かな配慮がなされている点が本当に素敵なライブラリだと強く感じました^^
5. まとめと謝辞
今回のサンプル作成を行っていくにあたり、一番の悩みどころは__「複数画像のFirebase Storageへのアップロードの部分」__でした。自前で実装すると考慮事項がかなり多い部分(実践だともっと色々出てきそう)でしたので、その部分があらかじめ考慮されていたことは非常に助かりましたし、Salada.swift
内で定義されているIngredient
クラスを継承したクラスの中にデータの変数名と型を定義しておいて管理する点等は非常に便利だと感じました。
今回はFirebaseのDatabaseとStorageの処理に関するものとSaladaを用いた実装に関する解説でおしまいではありますが、その他の機能に関しても、しっかりと試していきたいと感じた次第です。
そして記事作成にあたりましては「Salada」の作者でもある、@1_am_a_geek様より本サンプルの実装にあたりFirebaseでのファイルアップロード部分に関することやライブラリ「Salada」に関する部分に関して、多くのアドバイスやご協力を頂きました。 色々とご迷惑をおかけした点も多々あったかと思いますが、この場をお借り致しましてお礼申し上げます。本当にありがとうございました。
また今回のサンプルに関しては、Firebaseより画像をダウンロードを行う部分の作りこみや速度制限がかかった端末での処理部分などで作りが甘い部分がもしかしたらあるかもしれませんが、微力ではありますが実装の際に皆様のご参考の一助となれば幸いに思います。