Skip to content

Instantly share code, notes, and snippets.

@mironal
Last active September 21, 2018 11:55
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mironal/80297b07897f29cb3114e28d9658db47 to your computer and use it in GitHub Desktop.
Save mironal/80297b07897f29cb3114e28d9658db47 to your computer and use it in GitHub Desktop.
Swift で Firebase Database を安全に使う便利なやつたち. README.md にちょっと説明書いた.

Firebase Datbase の難しい & 嫌なところ

  • Database.database().reference() が長い
  • Database.database().reference().child("hoge").child(uid") って Query を作るのに間違いが発生しやすい
  • そのオブジェクト Readonly なの? それとも Read, Write なの?
    • firebase database の Rule によって出来る操作が違う
  • snapshot.value が Any だから辛い
  • snapshot.value のパース処理を書くのがだるい
  • 全体的に型がないから辛い

良くした

  • Query を作るところを隠蔽した -> Query だけ作ってテストできる
  • モデルの型が Unboxable に適合していれば snapshot.value のパース処理の実装が不要
    • SwiftFirebase.swiftpublic extension FirebaseModelRef where Model: Unboxable など
  • モデルの型に適合させる protocol によって Readonly や Read, Write な操作だけに出来る -> Rule をコードに反映できる
    • SystemStatus は Readonly, Configs は Read, Write
//
// Config.swift
// SwiftFirebase
//
// Created by mironal on 2017/05/27.
// Copyright © 2017年 covelline. All rights reserved.
//
import Foundation
import Unbox
import FirebaseDatabase.FIRDatabaseQuery
/*
readwrite なモデル.
{
"configs": { <- 全体を取得したいときは ConfigsRef を使って参照. この参照で childByAutoId() を使った書き込みもできる.
"uid": { <- 特定の uid の Config を取得したいときは ConfigRef を使って参照. この参照で書き込みもできる.
"canSomething": <boolean>
},
"uid": {
"canSomething": <boolean>
}
}
}
*/
struct Config {
let canSomething: Bool
}
extension Config: Unboxable {
init(unboxer: Unboxer) throws {
canSomething = try unboxer.unbox(key: "canSomething")
}
}
extension Config: FirebaseModel {}
extension Config: FirebaseStorable {
func firebaseValue() -> [AnyHashable : Any] {
return ["canSomething": canSomething]
}
}
/// "configs/{uid}" への参照
struct ConfigRef {
let uid: String
}
extension ConfigRef: FirebaseModelRef {
typealias Model = Config
var query: DatabaseQuery {
return rootRef.child("configs").child(uid)
}
}
extension ConfigRef: FirebaseCreatableValueRef {}
extension ConfigRef: FirebaseRemovableValueRef {}
/// "configs" への参照
struct ConfigsRef {}
extension ConfigsRef: FirebaseArrayModelRef {
typealias Model = Config
var query: DatabaseQuery {
return rootRef.child("configs")
}
}
extension ConfigsRef: FirebaseChildCreatableValueRef {}
struct OrderedConfigsRef: FirebaseArrayModelRef {
typealias Model = Config
var query: DatabaseQuery {
return rootRef.child("configs").queryOrderedByKey()
}
}
//
// SwiftFirebase.swift
// SwiftFirebase
//
// Created by mironal on 2017/05/27.
// Copyright © 2017年 covelline. All rights reserved.
//
import Foundation
import FirebaseDatabase
import Unbox
/// firebase に保存可能であることを示す
public protocol FirebaseStorable {
func firebaseValue() -> [AnyHashable: Any]
}
/// Firebase Database のモデル
public protocol FirebaseModel {}
/// Firebase Database のモデルへの参照
public protocol FirebaseRef {
/// この参照が示すモデルの型
associatedtype Model: FirebaseModel
/// Database.database().reference() の alias
var rootRef: DatabaseReference { get }
/// この参照への query
var query: DatabaseQuery { get }
}
public extension FirebaseRef {
var rootRef: DatabaseReference {
return Database.database().reference()
}
}
/// このモデルへの参照が値を作成可能であることを示す
/// 対象となるオブジェクトが既に FirebaseModelRef に適合している場合はデフォルトの実装が存在する
public protocol FirebaseCreatableValueRef: FirebaseRef {
associatedtype Model: FirebaseStorable
/// firebase の setValue(_ value: Any?, withCompletionBlock block: @escaping (Error?, DatabaseReference) -> Swift.Void)
func setValue(_ value: Model, withCompletionBlock block: @escaping (Error?, DatabaseReference) -> Swift.Void)
/// firebase の setValue(_ value: Any?, andPriority priority: Any?, withCompletionBlock block: @escaping (Error?, DatabaseReference) -> Swift.Void)
func setValue(_ value: Model, andPriority priority: Any?, withCompletionBlock block: @escaping (Error?, DatabaseReference) -> Swift.Void)
/// firebase の updateChildValues(_ values: [AnyHashable : Any], withCompletionBlock block: @escaping (Error?, DatabaseReference) -> Swift.Void)
func updateChildValues(_ values: Model, withCompletionBlock block: @escaping (Error?, DatabaseReference) -> Swift.Void)
}
/// このモデルへの参照が childByAutoId を使用した値を作成可能であることを示す
/// 対象となるオブジェクトが既に FirebaseArrayModelRef に適合している場合はデフォルトの実装が存在する
public protocol FirebaseChildCreatableValueRef: FirebaseRef {
associatedtype Model: FirebaseStorable
/// childByAutoId で value を作る
func setChildValue(_ value: Model, withCompletionBlock block: @escaping (Error?, DatabaseReference) -> Swift.Void)
/// firebase の func setValue(_ value: Any?, andPriority priority: Any?, withCompletionBlock block: @escaping (Error?, DatabaseReference) -> Swift.Void)
func setChildValue(_ value: Model, andPriority priority: Any?, withCompletionBlock block: @escaping (Error?, DatabaseReference) -> Swift.Void)
}
/// このモデルへの参照が値を削除可能であることを示す
/// 対象となるオブジェクトが既に FirebaseModelRef に適合している場合はデフォルトの実装が存在する
public protocol FirebaseRemovableValueRef: FirebaseRef {
/// firebase の removeValue(completionBlock block: @escaping (Error?, DatabaseReference) -> Swift.Void)
func removeValue(completionBlock block: @escaping (Error?, DatabaseReference) -> Swift.Void)
}
/// モデル単体への参照
public protocol FirebaseModelRef: FirebaseRef {
/// firebase の observeSingleEvent(of eventType: DataEventType, with block: @escaping (DataSnapshot) -> Swift.Void, withCancel cancelBlock: ((Error) -> Swift.Void)? = nil)
func observeSingleEvent(of eventType: DataEventType, with block: @escaping (Model, DataSnapshot) -> Swift.Void, withCancel cancelBlock: ((Error) -> Swift.Void)?)
/// firebase の observe(_ eventType: DataEventType, with block: @escaping (DataSnapshot) -> Swift.Void, withCancel cancelBlock: ((Error) -> Swift.Void)? = nil) -> UInt
func observe(_ eventType: DataEventType, with block: @escaping (Model, DataSnapshot) -> Swift.Void, withCancel cancelBlock: ((Error) -> Swift.Void)?) -> UInt
}
/// モデル単体集合への参照
public protocol FirebaseArrayModelRef: FirebaseRef {
/// firebase の observeSingleEvent(of eventType: DataEventType, with block: @escaping (DataSnapshot) -> Swift.Void, withCancel cancelBlock: ((Error) -> Swift.Void)? = nil)
func observeSingleEvent(of eventType: DataEventType, with block: @escaping ([String: Model], DataSnapshot) -> Swift.Void, withCancel cancelBlock: ((Error) -> Swift.Void)?)
/// firebase の observe(_ eventType: DataEventType, with block: @escaping (DataSnapshot) -> Swift.Void, withCancel cancelBlock: ((Error) -> Swift.Void)? = nil) -> UInt
func observe(_ eventType: DataEventType, with block: @escaping ([String: Model], DataSnapshot) -> Swift.Void, withCancel cancelBlock: ((Error) -> Swift.Void)?) -> UInt
}
enum SwiftFirebaseError: Error {
case paseError
}
// MARK - 便利なデフォルト実装達
public extension FirebaseModelRef where Model: Unboxable {
func observeSingleEvent(of eventType: DataEventType, with block: @escaping (Model, DataSnapshot) -> Swift.Void, withCancel cancelBlock: ((Error) -> Swift.Void)?) {
query.observeSingleEvent(of: eventType, with: { snapshot in
parse(snapshot: snapshot, block, withCancel: cancelBlock)
}, withCancel: cancelBlock)
}
func observe(_ eventType: DataEventType, with block: @escaping (Model, DataSnapshot) -> Swift.Void, withCancel cancelBlock: ((Error) -> Swift.Void)?) -> UInt {
return query.observe(eventType, with: { snapshot in
parse(snapshot: snapshot, block, withCancel: cancelBlock)
}, withCancel: cancelBlock)
}
}
public extension FirebaseArrayModelRef where Model: Unboxable {
func observeSingleEvent(of eventType: DataEventType, with block: @escaping ([String: Model], DataSnapshot) -> Swift.Void, withCancel
cancelBlock: ((Error) -> Swift.Void)? = nil) {
query.observeSingleEvent(of: eventType, with: { snapshot in
parse(snapshot: snapshot, block, withCancel: cancelBlock)
}, withCancel: cancelBlock)
}
func observe(_ eventType: DataEventType, with block: @escaping ([String: Model], DataSnapshot) -> Swift.Void, withCancel cancelBlock: ((Error) -> Swift.Void)?) -> UInt {
return query.observe(eventType, with: { snapshot in
parse(snapshot: snapshot, block, withCancel: cancelBlock)
}, withCancel: cancelBlock)
}
}
private func parse<Model: Unboxable>(snapshot: DataSnapshot, _ block: @escaping (Model, DataSnapshot) -> Swift.Void, withCancel cancelBlock: ((Error) -> Swift.Void)?) {
if let dictionary = snapshot.value as? [String: AnyObject], let model: Model = try? unbox(dictionary: dictionary) {
block(model, snapshot)
} else {
print("Parse error \(Model.self)")
cancelBlock?(SwiftFirebaseError.paseError)
}
}
private func parse<Model: Unboxable>(snapshot: DataSnapshot, _ block: @escaping ([String: Model], DataSnapshot) -> Swift.Void, withCancel cancelBlock: ((Error) -> Swift.Void)?) {
var error: SwiftFirebaseError?
let models: [String: Model] = snapshot.children.reduce([:], { (result, child) -> [String: Model] in
var result = result
if let child = child as? DataSnapshot,
let dict = child.value as? [String: Any],
let model: Model = try? unbox(dictionary: dict) {
result[child.key] = model
} else {
error = SwiftFirebaseError.paseError
print("Parse error \(Model.self)")
}
return result
})
if let error = error {
cancelBlock?(error)
} else {
block(models, snapshot)
}
}
public extension FirebaseChildCreatableValueRef where Self: FirebaseArrayModelRef {
var childRef: DatabaseReference {
return query.ref.childByAutoId()
}
func setChildValue(_ value: Model, withCompletionBlock block: @escaping (Error?, DatabaseReference) -> Swift.Void) {
childRef.setValue(value.firebaseValue(), withCompletionBlock: block)
}
func setChildValue(_ value: Model, andPriority priority: Any?, withCompletionBlock block: @escaping (Error?, DatabaseReference) -> Swift.Void) {
childRef.setValue(value.firebaseValue(), andPriority: priority, withCompletionBlock: block)
}
}
public extension FirebaseCreatableValueRef where Self: FirebaseModelRef {
func setValue(_ value: Model, withCompletionBlock block: @escaping (Error?, DatabaseReference) -> Swift.Void) {
query.ref.setValue(value.firebaseValue(), withCompletionBlock: block)
}
func setValue(_ value: Model, andPriority priority: Any?, withCompletionBlock block: @escaping (Error?, DatabaseReference) -> Swift.Void) {
query.ref.setValue(value.firebaseValue(), andPriority: priority, withCompletionBlock: block)
}
func updateChildValues(_ values: Model, withCompletionBlock block: @escaping (Error?, DatabaseReference) -> Swift.Void) {
query.ref.updateChildValues(values.firebaseValue(), withCompletionBlock: block)
}
}
public extension FirebaseRemovableValueRef where Self: FirebaseModelRef {
func removeValue(completionBlock block: @escaping (Error?, DatabaseReference) -> Swift.Void) {
query.ref.removeValue(completionBlock: block)
}
}
//
// SystemStatus.swift
// SwiftFirebase
//
// Created by mironal on 2017/05/27.
// Copyright © 2017年 covelline. All rights reserved.
//
import Foundation
import Unbox
import FirebaseDatabase.FIRDatabaseQuery
/*
readonly なモデル.
## model
{
"systemStatus": {
"good": <boolean>
}
}
## rule
{
"rules": {
".read": "false,
".write": "false"
"systemStatus": {
".read: "auth != nil"
}
}
}
*/
struct SystemStatus {
let good: Bool
}
extension SystemStatus: FirebaseModel {}
extension SystemStatus: Unboxable {
init(unboxer: Unboxer) throws {
good = try unboxer.unbox(key: "good")
}
}
struct SystemStatusRef {}
extension SystemStatusRef: FirebaseModelRef {
var query: DatabaseQuery {
return rootRef.child("systemStatus")
}
typealias Model = SystemStatus
}
//
// ViewController.swift
// SwiftFirebaseDemo
//
// Created by mironal on 2017/05/27.
// Copyright © 2017年 covelline. All rights reserved.
//
import UIKit
import FirebaseDatabase
class ViewController: UIViewController {
@IBAction func pushConfig(_ sender: Any) {
// Database.database().reference().child("configs").child("1").observeSingleEvent(of: ... & パース処理) が以下の1行で書ける
ConfigRef(uid: "1").observeSingleEvent(of: .value, with: { (config, snapshot) in
print(config)
}) { error in
print(error)
}
}
@IBAction func pushConfigs(_ sender: Any) {
ConfigsRef().observeSingleEvent(of: .value, with: { (configs, snapshot) in
print(configs)
}) { error in
print(error)
}
}
@IBAction func pushOrderdConfigs(_ sender: Any) {
OrderedConfigsRef().observeSingleEvent(of: .value, with: { (configs, snapshot) in
print(configs)
}) { error in
print(error)
}
}
@IBAction func pushCreateAutoId(_ sender: Any) {
let config = Config(canSomething: true)
ConfigsRef().setChildValue(config) { (error, reference) in
}
}
@IBAction func pushCreateSpecificId(_ sender: Any) {
let config = Config(canSomething: true)
ConfigRef(uid: "1").setValue(config) { (error, reference) in
}
}
@IBAction func pushRemoveConfig(_ sender: Any) {
ConfigRef(uid: "1").removeValue { (error, reference) in
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment