Skip to content

Instantly share code, notes, and snippets.

@fumiyasac
Last active December 3, 2022 11:59
Show Gist options
  • Save fumiyasac/4b51996fa17147c34eb02889de189229 to your computer and use it in GitHub Desktop.
Save fumiyasac/4b51996fa17147c34eb02889de189229 to your computer and use it in GitHub Desktop.
Firebase Realtime Database & FireStorage async/await Examples (Vol.1)
// ==========
// 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