Last active
December 3, 2022 11:59
-
-
Save fumiyasac/4b51996fa17147c34eb02889de189229 to your computer and use it in GitHub Desktop.
Firebase Realtime Database & FireStorage async/await Examples (Vol.1)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ========== | |
// Firebaseに関する実装Tips集 | |
// 👉 業務などで利用した経験の中でほんの少し役に立ったものを紹介しています。 | |
// ========== | |
// ---------- | |
/* 1. Realtime Database & FireStorageでShardingを実施する場合 */ | |
// Situation: | |
// 例えば用途に応じてDBやStorageの接続先を変えたい様な場合に利用する(ここでの例はStagingとProductionで分ける場合) | |
// Document: | |
// https://firebase.google.com/docs/database/usage/sharding | |
// ---------- | |
// ① FireStorageの場合 | |
import Foundation | |
import FirebaseStorage | |
final class FireStorageManager { | |
private (set)var storageReference: StorageReference! | |
// MARK: - Initializer | |
init() { | |
#if STAGING | |
let storageUrl = "gs://(Staging用).appspot.com/" | |
#else | |
let storageUrl = "gs://(Production用).appspot.com/" | |
#endif | |
self.storageReference = Storage.storage(url: storageUrl).reference() | |
} | |
// MARK: - Singleton | |
static let shared = FireStorageManager() | |
} | |
// ② Realtime Databaseの場合 | |
import Foundation | |
import FirebaseDatabase | |
final class RealtimeDatabaseManager { | |
private (set)var shopsReference: DatabaseReference! | |
private (set)var usersReference: DatabaseReference! | |
// MARK: - Initializer | |
init() { | |
#if STAGING | |
let shopsUrl = "https://(Shop用DBStaging用).firebaseio.com/" | |
let usersUrl = "https://(User用DBStaging用).firebaseio.com/" | |
#else | |
let shopsUrl = "https://(Shop用DBProduction用).firebaseio.com/" | |
let usersUrl = "https://(User用DBProduction用).firebaseio.com/" | |
#endif | |
let shopsDatabase = Database.database(url: shopsUrl) | |
shopsDatabase.isPersistenceEnabled = true | |
let usersDatabase = Database.database(url: usersUrl) | |
usersDatabase.isPersistenceEnabled = true | |
self.shopsReference = shopsDatabase.reference() | |
self.usersReference = usersDatabase.reference() | |
} | |
// MARK: - Singleton | |
static let shared = RealtimeDatabaseManager() | |
} | |
// ---------- | |
/* 2. Realtime Databaseでasync/awaitを利用した処理例 */ | |
// Situation: | |
// 一覧データを全て取得する | |
// Document: | |
// https://firebase.google.com/docs/reference/swift/firebasedatabase/api/reference/Classes/DatabaseQuery#getdata | |
// ---------- | |
// ① Shopsテーブルにおけるデータ登録例 | |
/* | |
下記の様な形で『Key-Value Object』が登録されている。 | |
https://(Shop用DB).firebaseio.com/ | |
- shops | |
- 0 | |
- id: 1000001 | |
- name: "美味しいイタリアンのお店" | |
- shop_image_url: "(お店の画像URL)" | |
- updated_at: "2022-10-22T12:00:00+09:00" | |
- foods | |
- 0 | |
- id: "1000001_main_food" | |
- image_url: "(イチオシ商品画像URL)" | |
- uploaded_user: "user000001" | |
- 1 | |
- id: "1000001_sub1_food" | |
- image_url: "(その他商品画像URLその1)" | |
- uploaded_user: "user000001" | |
- 2 | |
- id: "1000001_sub2_food" | |
- image_url: "(その他商品画像URLその2)" | |
- uploaded_user: "user000001" | |
- 3 | |
- id: "1000001_sub3_food" | |
- image_url: "(その他商品画像URLその3)" | |
- uploaded_user: "user000001" | |
- 4 | |
- id: "1000001_sub4_food" | |
- image_url: "(その他商品画像URLその4)" | |
- uploaded_user: "user000001" | |
・・・(この様なデータが以後続く)・・・ | |
👉 このことを踏まえてコードに落とし込むと下記の様な形となる | |
*/ | |
import Foundation | |
import FirebaseDatabase | |
import FirebaseDatabaseSwift | |
protocol GetShopsUseCase { | |
func getAll() async throws -> [Shop] | |
} | |
final class GetShopsUseCaseImpl: GetShopsUseCase { | |
// MARK: - typealias | |
typealias keyValueObject = [Shop] | |
// MARK: - Function | |
func getAll() async throws -> [Shop] { | |
let childName = "shops" | |
// 👉 getData()メソッドを利用してasync/awaitをベースにしたデータ取得処理をする | |
let snapshot = try await RealtimeDatabaseManager.shared.storyMonitorShopsReference.child(childName).getData() | |
// 👉 data(as: keyValueObject.self)メソッドを利用してAPIレスポンスをCodableでマッピングする際と同様なイメージで処理が可能 | |
// ※ FirebaseDatabaseSwiftをimportする必要があります。 | |
guard let shops = try? snapshot.data(as: keyValueObject.self) else { | |
return [] | |
} | |
return shops | |
} | |
} | |
// MARK: - Struct (Shop) | |
struct Shop: Codable { | |
let shopID: Int | |
let name: String | |
let shopImageUrl: URL? | |
let updatedAt: String | |
let foodImages: [FoodImage] | |
enum CodingKeys: String, CodingKey { | |
case shopID = "id" | |
case name | |
case shopImageUrl = "shop_image_url" | |
case updatedAt = "updated_at" | |
case foodImages = "foods" | |
} | |
} | |
// MARK: - Struct (FoodImage) | |
struct FoodImage: Codable { | |
let id: String | |
let uploadedUser: String | |
let imageUrl: URL? | |
enum CodingKeys: String, CodingKey { | |
case id | |
case uploadedUser = "uploaded_user" | |
case imageUrl = "image_url" | |
} | |
} | |
// ② Usersテーブルにおけるデータ登録例 | |
/* | |
下記の様な形で『Key-Value Object』が登録されている。 | |
https://(User用DB).firebaseio.com/ | |
- users | |
- 1000001 (👉 この部分がUserのIDとなっている点に注意!) | |
- name: "ユーザー1号" | |
- user_rank: "SSS" | |
- available_point: 18000 | |
- avatar_image_url: "(アバター画像URL)" | |
- created_at: "2022-10-22T12:00:00+09:00" | |
- 1000002 | |
- name: "ユーザー2号" | |
- user_rank: "A" | |
- available_point: 750 | |
- avatar_image_url: "(アバター画像URL)" | |
- created_at: "2022-10-22T12:00:00+09:00" | |
- 1000003 | |
- name: "ユーザー3号" | |
- user_rank: "SS" | |
- available_point: 1200 | |
- avatar_image_url: "(アバター画像URL)" | |
- created_at: "2022-10-22T12:00:00+09:00" | |
・・・(この様なデータが以後続く)・・・ | |
👉 このことを踏まえてコードに落とし込むと下記の様な形となる | |
*/ | |
import Foundation | |
import FirebaseDatabase | |
import FirebaseDatabaseSwift | |
protocol GetUsersUseCase { | |
func getAll() async throws -> [User] | |
} | |
final class GetUsersUseCaseImpl: GetUsersUseCase { | |
// MARK: - typealias | |
typealias keyValueObject = [String: UserInformation] | |
// MARK: - Function | |
func getAll() async throws -> [User] { | |
let childName = "users" | |
// 👉 getData()メソッドを利用してasync/awaitをベースにしたデータ取得処理をする | |
let snapshot = try await RealtimeDatabaseManager.shared.storyMonitorUsersReference.child(childName).getData() | |
// 👉 data(as: keyValueObject.self)メソッドを利用してAPIレスポンスをCodableでマッピングする際と同様なイメージで処理が可能 | |
// ※ FirebaseDatabaseSwiftをimportする必要があります。 | |
guard let dictionary = try? snapshot.data(as: keyValueObject.self) else { | |
return [] | |
} | |
// 👉 key部分がユーザーID文字列なのでその点に注意する | |
let targetUsers = dictionary.map { (userID, userInformation) in | |
User(targetUserID: Int(userID) ?? 0, userInformation: userInformation) | |
} | |
return targetUsers | |
} | |
} | |
// MARK: - Struct (User) | |
struct User { | |
let targetUserID: Int | |
let name: String | |
let userRank: String | |
let availablePoint: Int | |
let avatarImageUrl: URL? | |
let createdAt: String | |
init(targetUserID: Int, userInformation: UserInformation) { | |
self.targetUserID = targetUserID | |
self.name = userInformation.name | |
self.userRank = userInformation.userRank | |
self.availablePoint = userInformation.availablePoint | |
self.avatarImageUrl = userInformation.avatarImageUrl | |
self.createdAt = userInformation.createdAt | |
} | |
} | |
// MARK: - Struct (UserInformation) | |
// MEMO: この構造体は取得できたsnapshotにおける、Dictionaryのvalueに該当する部分 | |
struct UserInformation: Codable { | |
let name: String | |
let userRank: String | |
let availablePoint: Int | |
let avatarImageUrl: URL? | |
let createdAt: String | |
enum CodingKeys: String, CodingKey { | |
case name | |
case userRank = "user_rank" | |
case availablePoint = "available_point" | |
case avatarImageUrl = "avatar_image_url" | |
case createdAt = "created_at" | |
} | |
} | |
// ---------- | |
/* 3. FireStorageでasync/awaitを利用した処理例 */ | |
// Situation: | |
// 複数のUIImageデータ(画像データ)をFireStorageへ順番に基づいた名前を付けてアップロードする | |
// Document: | |
// https://firebase.google.com/docs/reference/swift/firebasestorage/api/reference/Classes/StorageReference#putdataasync_:metadata: | |
// ---------- | |
import Foundation | |
import FirebaseStorage | |
import UIKit | |
protocol PostShopImagesUseCase { | |
func uploadShopImages(shopID: String, images: [UIImage]) async -> UploadImagesResult | |
} | |
// MARK: - Enum (for File Upload Result) | |
enum UploadImagesResult { | |
case success | |
case failure | |
} | |
final class PostShopImagesUseCaseImpl: PostShopImagesUseCase { | |
// MARK: - Function | |
func uploadShopImages(shopID: String, images: [UIImage]) async -> UploadImagesResult { | |
return await uploadImagesToFireStorage(shopID: shopID, images: images) | |
} | |
// MARK: - Private Function | |
private func uploadImagesToFireStorage(shopID: String, images: [UIImage]) async -> UploadImagesResult { | |
// 👉 画像アップロードディレクトリ名・ファイル名で利用するための日付文字列を取得する | |
let dateFormatter: DateFormatter = { | |
var dateFormatter = DateFormatter() | |
dateFormatter.dateFormat = "yyyyMMdd" | |
return dateFormatter | |
}() | |
let uploadDateString = dateFormatter.string(from: Date()) | |
// 👉 画像アップロード処理用の本丸部分 | |
var uploadSuccessCount: Int = 0 | |
for (i, image) in images.enumerated() { | |
// 👉 PHPickerViewControllerやUIImagePickerControllerから取得した画像データをアップロードする前準備 | |
// 引数で受け取ったUIImageをData型に変換する(compressionQualityの値は仕様に応じて決定する) | |
guard let data = image.jpegData(compressionQuality: 0.5) else { | |
assertionFailure("jpegへの変換に失敗しました。") | |
break | |
} | |
// Metadataを設定する | |
let metadata = StorageMetadata() | |
metadata.contentType = "image/jpeg" | |
// 👉 アップロードする画像を配置する場所を設定する(配置するパス情報については仕様に応じて決定する) | |
// Path例: user_shop_images/{shopID}/{userID}/{YYYYMMDD}/{YYYYMMDD_(1...5).jpg} | |
let imageIndex = index + 1 | |
let imagePath = "user_shop_images/" | |
+ "\(shopID)/" | |
+ "\(getUserID())/" | |
+ "\(uploadDateString)/" | |
let imageDirectory = imagePath + "\(uploadDateString)_\(imageIndex).jpg" | |
// 👉 FirestorageへputDataAsyncを利用して画像ファイルアップロード処理を実行する | |
// ※ for文のループ処理を実行している最中は、画面上はLoadingIndicatorが表示されている想定をして実装する様にする。 | |
guard let reference = FireStorageManager.shared.storageReference?.child(imageDirectory) else { | |
assertionFailure("referenceの設定に失敗しました。") | |
break | |
} | |
do { | |
// MEMO: ファイルアップロード処理に成功した場合はこの場合はuploadSuccessCountのカウントをインクリメントしている。 | |
_ = try await reference.putDataAsync(data, metadata: metadata) | |
uploadSuccessCount += 1 | |
print("File Upload Success: " + "Uploaded Count is " + String(describing: uploadSuccessCount)) | |
} catch let error { | |
// MEMO: 厳密にはファイルアップロード処理に失敗した画像があればログを送信する等をしておいた方がが望ましい。 | |
print("File Upload Error: " + error.localizedDescription) | |
} | |
} | |
// 👉 成功時or失敗時のハンドリング処理を実行する | |
// ※ この処理ではアップロードに成功したものが1つでもあった場合には成功と見なしているが、返り値とその内容については仕様に応じて決定するのが望ましい。 | |
return (uploadSuccessCount > 0) ? .success : .failure | |
} | |
private func getUserID() -> String { | |
// (実際の処理は割愛しています) UserIDを取得する処理 | |
} | |
} | |
// ========== | |
// 次回. その他にもasync/awaitができるもの探してみる(FirestoreやFirebaseAuth等についても調べる) | |
// 👉 他にも可能な余地があるなら進んで利用をしていきたい部分 | |
// 参考リンク: | |
// https://tech.nri-net.com/entry/firebase_swift_async_await | |
// https://www.fuwamaki.com/article/342 | |
// ========== |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment