Ballcap 1.3.5

Ballcap 1.3.5

1amageek 维护。



 
依赖项
Firebase>= 0
Firebase/Firestore>= 0
Firebase/Storage>= 0
FirebaseFirestoreSwift>= 0
 

Ballcap 1.3.5

🧢Ballcap-iOS

Version Platform Downloads

Ballcap 是 Cloud Firestore 数据库模式设计框架。

为何选择 Ballcap

Cloud Firestore 是一个很棒的无模式且灵活的数据库,可以处理数据。然而,它的灵活性可能在开发中引发许多错误。Ballcap 可以将模式分配给 Cloud Firestore 以可视化数据结构。这在团队开发中扮演着非常重要的角色。

灵感来源于 https://github.com/firebase/firebase-ios-sdk/tree/pb-codable3

请捐款以继续开发。

示例项目

  • Messagestore 使用 Cloud Firestore 创建的聊天框架。

功能

☑️使用 Swift Codable 的 Firestore 文档模式
☑️当然,类型安全。
☑️与 Firestore 和 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 不一定有数据。在某些条件下可能没有数据。

  1. 如果 DocumentReference 中没有存储数据。
  2. 如果使用 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?)
  • PDF
  • 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()
}