Awesome
<p align='center'> <img src='https://raw.githubusercontent.com/sgr-ksmt/FireSnapshot/master/assets/logo.png' width='600px' /> </p> <div align='center'>A useful Firebase-Cloud-Firestore Wrapper with Codable.
Developed by @sgr-ksmt
</div> <hr />Table of Contents <!-- omit in toc -->
<hr />Feature
- 🙌 Support Codable (Use
FirebaseFirestoreSwift
inside). - 🙌 Provide easy-to-use methods for CRUD, Batch, Transaction.
- 🙌 Support
array-union/array-remove
. - 🙌 Support
FieldValue.increment
. - 🙌 Support
FieldValue.delete()
. - 🙌 Support KeyPath based query.
Use Swift features(version: 5.1) <!-- omit in toc -->
<hr />Usage
Basic Usage
The type of Document
must be conformed to the SnapshotData
protocol.
SnapshotData
protocol inherits Codable
. For example:
struct Product: SnapshotData {
var name: String = ""
var desc: String?
var price: Int = 0
var attributes: [String: String] = [:]
}
It is convenient to define DocumentPath<T>
and CollectionPath<T>
.
Define path for extension of DocumentPaths
or CollectionPaths
.
extension CollectionPaths {
static let products = CollectionPath<Product>("products")
}
extension DocumentPaths {
static func product(_ productID: String) -> DocumentPath<Product> {
CollectionPaths.products.document(productID)
}
}
Create Snapshot
with model that comformed to SnapshotData
and path.
let product = Snapshot<Product>(data: Product(), path: CollectionPath.products)
In short 👇
let product = Snapshot(data: .init(), path: .products)
You can save it by calling create(completion:)
product.create { error in
if let error = error {
print("error", error)
return
}
print("created!")
}
FireSnapshot
also provides read(get document(s)/listen document(s)
), write(update/delete
), write with batch and transaction
// Update document
product.update { error in
if let error = error {
print("error", error)
return
}
print("updated!")
}
// Delete document
product.delete { error in
if let error = error {
print("error", error)
return
}
print("deleted!")
}
// Get document
Snapshot.get(.product("some_product_id")) { result in
switch result {
case let .success(product):
print(product.name)
case let .failure(error):
print(error)
}
}
// Listen document
let listener = Snapshot.listen(.product("some_product_id")) { result in
switch result {
case let .success(product):
print("listened new product", product.name)
case let .failure(error):
print(error)
}
}
// Get documents
Snapshot.get(.products) { result in
switch result {
case let .success(products):
print(products.count)
case let .failure(error):
print(error)
}
}
// Listen documents
let listener = Snapshot.listen(.products) { result in
switch result {
case let .success(products):
print("listened new products", products.count)
case let .failure(error):
print(error)
}
}
If you can read/write timestamp such as createTime
and updateTime
, model must be conform to HasTimestamps
protocol.
struct Product: SnapshotData, HasTimestamps {
var name: String = ""
var desc: String?
var price: Int = 0
var attributes: [String: String] = [:]
}
let product = Snapshot(data: .init(), path: .products)
// `createTime` and `updateTime` will be written to field with other properties.
product.create()
Snapshot.get(product.path) { result in
guard let p = try? result.get() else {
return
}
// optional timestamp value.
print(p.createTime)
print(p.updateTime)
// `updateTime` will be updated with other properties.
p.update()
}
<hr />
Advanced Usage
@IncrementableInt
/ @IncrementableDouble
<!-- omit in toc -->
If you want to use FieldValue.increment
on model, use @IncrementableInt(Double)
.
- The type of
@IncrementableInt
property isInt64
. - The type of
@IncrementableDouble
property isDouble
.
extension CollectionPaths {
static let products = CollectionPath<Model>("models")
}
struct Model: SnapshotData {
@IncrementableInt var count = 10
@IncrementableDouble var distance = 10.0
}
Snapshot.get(.model(modelID)) { result in
guard let model = try? result.get() else {
return
}
// Refer a number
print(model.count) // print `10`.
print(model.distance) // print `10.0`.
// Increment (use `$` prefix)
model.$count.increment(1)
print(model.count) // print `11`.
model.update()
model.$distance.increment(1.0)
print(model.distance) // print `11.0`.
model.update()
// Decrement
model.$count.increment(-1)
print(model.count) // print `9`.
model.update()
model.$distance.increment(-1.0)
print(model.distance) // print `9.0`.
model.update()
// if you want to reset property, use `reset` method.
model.$count.reset()
}
@AtomicArray
<!-- omit in toc -->
If you want to use FieldValue.arrayUnion
or FieldValue.arrayRemove
, use @AtomicArray
.
The type of @AtomicArray
's element must be conformed to Codable
protocol.
extension CollectionPaths {
static let products = CollectionPath<Model>("models")
}
struct Model: SnapshotData {
@AtomicArray var languages: [String] = ["en", "ja"]
}
Snapshot.get(.model(modelID)) { result in
guard let model = try? result.get() else {
return
}
// Refer an array
print(model.languages) // print `["en", "ja"]`.
// Union element(s)
model.$languages.union("zh")
print(model.count) // print `["en", "ja", "zh"]`.
model.update()
// Remove element(s)
model.$languages.remove("en")
print(model.count) // print `["ja"]`.
model.update()
// if you want to reset property, use `reset` method.
model.$languages.reset()
}
@DeletableField
<!-- omit in toc -->
IF you want to use FieldValue.delete
, use @DeletableField
.
extension CollectionPaths {
static let products = CollectionPath<Model>("models")
}
struct Model: SnapshotData {
var bio: DeletableField<String>? = .init(value: "I'm a software engineer.")
}
Snapshot.get(.model(modelID)) { result in
guard let model = try? result.get() else {
return
}
print(model.bio?.value) // print `Optional("I'm a software engineer.")`
// Delete property
model.bio.delete()
model.update()
}
// After updated
Snapshot.get(.model(modelID)) { result in
guard let model = try? result.get() else {
return
}
print(model.bio) // nil
print(model.bio?.value) // nil
}
NOTE:
Normally, when property is set to nil, {key: null}
will be written to document,
but when using FieldValue.delete
, field of key
will be deleted from document.
KeyPath-based query <!-- omit in toc -->
You can use KeyPath-based query generator called QueryBuilder
if the model conform to FieldNameReferable
protocol.
extension CollectionPaths {
static let products = CollectionPath<Product>("products")
}
struct Product: SnapshotData, HasTimestamps {
var name: String = ""
var desc: String?
var price: Int = 0
var deleted: Bool = false
var attributes: [String: String] = [:]
}
extension Product: FieldNameReferable {
static var fieldNames: [PartialKeyPath<Mock> : String] {
return [
\Self.self.name: "name",
\Self.self.desc: "desc",
\Self.self.price: "price",
\Self.self.deleted: "deleted",
]
}
}
Snapshot.get(.products, queryBuilder: { builder in
builder
.where(\.price, isGreaterThan: 5000)
.where(\.deleted, isEqualTo: false)
.order(by: \.updateTime, descending: true)
}) { result in
...
}
<hr />
Installation
- CocoaPods
pod 'FireSnapshot', '~> 0.11.1'
<hr />
Dependencies
- Firebase:
v6.12.0
or higher. - FirebaseFirestoreSwift: Fetch from master branch.
- Swift:
5.1
or higher.
Road to 1.0
- Until 1.0 is reached, minor versions will be breaking 🙇.
Development
Setup <!-- omit in toc -->
$ git clone ...
$ cd path/to/FireSnapshot
$ make
$ open FireSnapshot.xcworkspace
Unit Test <!-- omit in toc -->
Start Firestore Emulator
before running Unit Test.
$ npm install -g firebase-tools
$ firebase setup:emulators:firestore
$ cd ./firebase/
$ firebase emulators:start --only firestore
# Open Xcode and run Unit Test after running emulator.
or, run ./scripts/test.sh
.
Communication
- If you found a bug, open an issue.
- If you have a feature request, open an issue.
- If you want to contribute, submit a pull request.:muscle:
Credit
FireSnapshot was inspired by followings:
<hr />License
FireSnapshot is under MIT license. See the LICENSE file for more info.