🧢 Ballcap-iOS
Ballcap 是 Cloud Firestore 数据库模式设计框架。
为何选择 Ballcap
Cloud Firestore 是一个很棒的无模式且灵活的数据库,可以处理数据。然而,它的灵活性可能在开发中引发许多错误。Ballcap 可以将模式分配给 Cloud Firestore 以可视化数据结构。这在团队开发中扮演着非常重要的角色。
灵感来源于 https://github.com/firebase/firebase-ios-sdk/tree/pb-codable3
- Ballcap for TypeScript: https://github.com/1amageek/ballcap.ts
示例项目
- Messagestore 使用 Cloud Firestore 创建的聊天框架。
功能
❗️
要求- iOS 10 或更高版本
- Swift 5.0 或更高版本
- Firebase Firestore
- Firebase Storage
⚙
安装CocoaPods
- 在您的Podfile中插入
pod 'Ballcap'
。 - 运行
pod install
。
如果您有功能请求,请提交issue。
使用方法
文档方案
您必须遵守 Codable 和 Modelable 协议以定义 Scheme。
struct Model: Codable, Equatable, Modelable {
var number: Int = 0
var string: String = "Ballcap"
}
初始化
文档初始化如下
// use auto id
let document: Document<Model> = Document()
print(document.data?.number) // 0
print(document.data?.string) // "Ballcap"
// KeyPath
print(document[\.number]) // 0
print(document[\.string]) // "Ballcap"
// Use your specified ID
let document: Document<Model> = Document(id: "YOUR_ID")
print(document.data?.number) // 0
print(document.data?.string) // "Ballcap"
// KeyPath
print(document[\.number]) // 0
print(document[\.string]) // "Ballcap"
// Use your specified DocumentReference
let documentReference: DocumentReference = Firestore.firestore().document("collection/document")
// note: If DocumentReference is specified, data is initialized with nil.
let document: Document<Model> = Document(documentReference)
print(document.data?.number) // nil
print(document.data?.string) // nil
// KeyPath
print(document[\.number]) // fail
print(document[\.string]) // fail
CRUD
Ballcap 具有内部缓存。当使用缓存时,使用 Batch
替代 WriteBatch
。
// save
document.save()
// update
document.update()
// delete
document.delete()
// Batch
let batch: Batch = Batch()
batch.save(document: document)
batch.update(document: document)
batch.delete(document: document)
batch.commit()
您可以通过使用获取函数来获取数据。
Document<Model>.get(id: "DOCUMENT_ID", completion: { (document, error) in
print(document.data)
})
下一个获取函数会优先获取缓存中的数据。如果没有缓存数据,它会从服务器获取。
let document: Document<Model> = Document("DOCUMENT_ID")
document.get { (document, error) in
print(document.data)
}
为什么数据是可选的?
在 CloudFirestore 中,DocumentReference 不一定有数据。在某些条件下可能没有数据。
- 如果 DocumentReference 中没有存储数据。
- 如果使用 DocumentReference 的
Source.cache
获取数据,但缓存中没有数据。
Ballcap 建议开发者如果可以确定有数据,则使用 unwarp。
也可以在不使用网络的情况下访问缓存。
let document: Document<Model> = Document(id: "DOCUMENT_ID")
print(document.cache?.number) // 0
print(document.cache?.string) // "Ballcap"
自定义属性
球帽正在准备与 FieldValue 对应的自定义属性。
ServerTimestamp
用于处理 FieldValue.serverTimestamp()
的属性
struct Model: Codable, Equatable {
let serverValue: ServerTimestamp
let localValue: ServerTimestamp
}
let model = Model(serverValue: .pending,
localValue: .resolved(Timestamp(seconds: 0, nanoseconds: 0)))
IncrementableInt & IncrementableDouble
用于处理 FieldValue.increment()
的属性
struct Model: Codable, Equatable, Modelable {
var num: IncrementableInt = 0
}
let document: Document<Model> = Document()
document.data?.num = .increment(1)
OperableArray
用于处理 FieldValue.arrayRemove()
和 FieldValue.arrayUnion()
的属性
struct Model: Codable, Equatable, Modelable {
var array: OperableArray<Int> = [0, 0]
}
let document: Document<Model> = Document()
document.data?.array = .arrayUnion([1])
document.data?.array = .arrayRemove([1])
文件
文件是用于访问 Firestorage 的一个类。您可以通过以下方法在文档相同路径下保存数据:
let document: Document<Model> = Document(id: "DOCUMENT_ID")
let file: File = File(document.storageReference)
文件支持多种 MIME 类型。尽管文件可以从名称推断 MIME 类型,但最好显式输入 MIME 类型。
- 纯文本
- CSV
- HTML
- CSS
- JavaScript
- 字节流(String?)
- ZIP
- TAR
- LZH
- jpeg
- ppjpeg
- png
- gif
- mp4
- 自定义(String, String)
上传 & 下载
上传和下载每个都返回一个任务。您可以通过访问任务来管理进度。
// upload
let ref: StorageReference = Storage.storage().reference().child("/a")
let data: Data = "test".data(using: .utf8)!
let file: File = File(ref, data: data, name: "n", mimeType: .plain)
let task = file.save { (metadata, error) in
}
// download
let task = file.getData(completion: { (data, error) in
let text: String = String(data: data!, encoding: .utf8)!
})
StorageBatch
当需要上传多个文件到云存储时使用 StorageBatch。
let textData: Data = "test".data(using: .utf8)!
let textFile: File = File(Storage.storage().reference(withPath: "c"), data: textData, mimeType: .plain)
batch.save(textFile)
let jpgData: Data = image.jpegData(compressionQuality: 1)!
let jpgFile: File = File(Storage.storage().reference(withPath: "d"), jpgData: textData, mimeType: .jpeg)
batch.save(jpgFile)
batch.commit { error in
}
DataSource
球帽提供了一个数据源,以便轻松处理集合和子集合。
数据源初始化
来自文档
let dataSource: DataSource<Item> = Document<Item>.query.dataSource()
来自集合参考
集合参考
let query: DataSource<Document<Item>>.Query = DataSource.Query(Firestore.firestore().collection("items"))
let dataSource = DataSource(reference: query)
集合组
let query: DataSource<Document<Item>>.Query = DataSource.Query(Firestore.firestore().collectionGroup("items"))
let dataSource = DataSource(reference: query)
您的自定义对象
// from Custom class
let dataSource: DataSource<Item> = Item.query.dataSource()
// from CollectionReference
let query: DataSource<Item>.Query = DataSource.Query(Item.collectionReference)
let dataSource: DataSource<Item> = query.dataSource()
NSDiffableDataSourceSnapshot
self.dataSource = Document<Item>.query
.order(by: "updatedAt", descending: true)
.limit(to: 3)
.dataSource()
.retrieve(from: { (snapshot, documentSnapshot, done) in
let document: Document<Item> = Document(documentSnapshot.reference)
document.get { (item, error) in
done(item!)
}
})
.onChanged({ (snapshot, dataSourceSnapshot) in
var snapshot: NSDiffableDataSourceSnapshot<Section, DocumentProxy<Item>> = self.tableViewDataSource.snapshot()
snapshot.appendItems(dataSourceSnapshot.changes.insertions.map { DocumentProxy(document: $0)})
snapshot.deleteItems(dataSourceSnapshot.changes.deletions.map { DocumentProxy(document: $0)})
snapshot.reloadItems(dataSourceSnapshot.changes.modifications.map { DocumentProxy(document: $0)})
self.tableViewDataSource.apply(snapshot, animatingDifferences: true)
})
.listen()
UITableViewDelegate, UITableViewDataSource
self.dataSource = Document<Item>.query
.order(by: "updatedAt", descending: true)
.limit(to: 3)
.dataSource()
.retrieve(from: { (snapshot, documentSnapshot, done) in
let document: Document<Item> = Document(documentSnapshot.reference)
document.get { (item, error) in
done(item!)
}
})
.onChanged({ (snapshot, dataSourceSnapshot) in
self.tableView.performBatchUpdates({
self.tableView.insertRows(at: dataSourceSnapshot.changes.insertions.map { IndexPath(item: dataSourceSnapshot.after.firstIndex(of: $0)!, section: 0)}, with: .automatic)
self.tableView.deleteRows(at: dataSourceSnapshot.changes.deletions.map { IndexPath(item: dataSourceSnapshot.before.firstIndex(of: $0)!, section: 0)}, with: .automatic)
self.tableView.reloadRows(at: dataSourceSnapshot.changes.modifications.map { IndexPath(item: dataSourceSnapshot.after.firstIndex(of: $0)!, section: 0)}, with: .automatic)
}, completion: nil)
})
.listen()
文档与对象之间的关系
Document
是从 Object
继承来的一个类。对于简单的操作,使用 Document
就足够了。
public final class Document<Model: Modelable & Codable>: Object, DataRepresentable, DataCacheable {
public var data: Model?
}
通过扩展 Object
并定义自己的类,您可以执行复杂操作。使用示例在 使用 Ballcap 与 SwiftUI 中解释。
Pring 迁移
从概述
与 Pring 的区别在于已经取消了 ReferenceCollection 和 NestedCollection。在 Pring 中,向父对象的 ReferenceCollection 和 NestedCollection 添加子对象时,当保存父对象时也会保存父对象。Ballcap 需要开发者使用 Batch 保存子集合。此外,Pring 还在保存包含文件的对象的同时保存文件。Ballcap 需要开发者使用 StorageBatch 保存文件。
方案
Ballcap 可以通过继承 Object 类来处理像 Pring 一样的 Object 类。如果您继承了 Object 类,则必须符合 DataRepresentable
。
class Room: Object, DataRepresentable {
var data: Model?
struct Model: Modelable & Codable {
var members: [String] = []
}
}
子集合
Ballcap 已经停止使用 NestedCollection 和 ReferenceCollection 类。相反,它通过定义 CollectionKeys 来表示子集合。
类必须匹配 HierarchicalStructurable
才能使用 CollectionKeys。
class Room: Object, DataRepresentable & HierarchicalStructurable {
var data: Model?
var transcripts: [Transcript] = []
struct Model: Modelable & Codable {
var members: [String] = []
}
enum CollectionKeys: String {
case transcripts
}
}
使用集合功能来访问子集合。
let collectionReference: CollectionReference = obj.collection(path: .transcripts)
子集合的文档保存
let batch: Batch = Batch()
let room: Room = Room()
batch.save(room.transcripts, to: room.collection(path: .transcripts))
batch.commit()
在 SwiftUI 中使用 Ballcap
首先,创建一个遵守 ObservableObject
协议的对象。《DataListenable》协议使对象可观察。
final class User: Object, DataRepresentable, DataListenable, ObservableObject, Identifiable {
typealias ID = String
override class var name: String { "users" }
struct Model: Codable, Modelable {
var name: String = ""
}
@Published var data: User.Model?
var listener: ListenerRegistration?
}
接下来,创建一个显示该对象的 View
。
struct UserView: View {
@ObservedObject var user: User
@State var isPresented: Bool = false
var body: some View {
VStack {
Text(user[\.name])
}
.navigationBarTitle(Text("User"))
.navigationBarItems(trailing: Button("Edit") {
self.isPresented.toggle()
})
.sheet(isPresented: $isPresented) {
UserEditView(user: self.user.copy(), isPresented: self.$isPresented)
}
.onAppear {
_ = self.user.listen()
}
}
}
您可以通过 subscript
访问对象数据。
Text(user[\.name])
使用 onAppear
开始用户观察。
.onAppear {
_ = self.user.listen()
}
复制对象
在编辑数据之前,将对象的一个副本传递给 EditView。
.sheet(isPresented: $isPresented) {
UserEditView(user: self.user.copy(), isPresented: self.$isPresented)
}
由于对象被观察者观察,因此可以自动捕获更改。
最后,创建一个可以更新对象的视图。
struct UserEditView: View {
@ObservedObject var user: User
@Binding var isPresented: Bool
var body: some View {
VStack {
Form {
Section(header: Text("Name")) {
TextField("Name", text: $user[\.name])
}
}
Button("Save") {
self.user.update()
self.isPresented.toggle()
}
}.frame(height: 200)
}
}
只有通过 update()
才能更新对象。
Button("Update") {
self.user.update()
self.isPresented.toggle()
}