{"id":23152403,"url":"https://github.com/1amageek/ballcap-ios","last_synced_at":"2025-04-07T07:17:30.515Z","repository":{"id":56251506,"uuid":"149568469","full_name":"1amageek/Ballcap-iOS","owner":"1amageek","description":"Firebase Cloud Firestore support library for iOS. 🧢","archived":false,"fork":false,"pushed_at":"2023-05-28T00:20:44.000Z","size":1288,"stargazers_count":231,"open_issues_count":9,"forks_count":28,"subscribers_count":14,"default_branch":"master","last_synced_at":"2025-03-31T06:04:54.332Z","etag":null,"topics":["cloudfirestore","firebase","firebase-firestore","firebase-storage","gcp","swift"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/1amageek.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null},"funding":{"github":"1amageek","patreon":null,"open_collective":null,"ko_fi":null,"tidelift":null,"community_bridge":null,"liberapay":null,"issuehunt":"1amageek","otechie":null,"custom":null}},"created_at":"2018-09-20T07:20:59.000Z","updated_at":"2025-02-25T15:02:48.000Z","dependencies_parsed_at":"2024-01-02T23:55:07.297Z","dependency_job_id":null,"html_url":"https://github.com/1amageek/Ballcap-iOS","commit_stats":{"total_commits":333,"total_committers":8,"mean_commits":41.625,"dds":"0.033033033033033066","last_synced_commit":"7159e0f5112a0e8fab52706aad02b466458a8308"},"previous_names":[],"tags_count":94,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/1amageek%2FBallcap-iOS","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/1amageek%2FBallcap-iOS/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/1amageek%2FBallcap-iOS/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/1amageek%2FBallcap-iOS/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/1amageek","download_url":"https://codeload.github.com/1amageek/Ballcap-iOS/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247608160,"owners_count":20965953,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cloudfirestore","firebase","firebase-firestore","firebase-storage","gcp","swift"],"created_at":"2024-12-17T19:14:33.900Z","updated_at":"2025-04-07T07:17:30.485Z","avatar_url":"https://github.com/1amageek.png","language":"Swift","funding_links":["https://github.com/sponsors/1amageek","https://issuehunt.io/r/1amageek"],"categories":[],"sub_categories":[],"readme":"# 🧢 Ballcap-iOS\n\n\u003cimg src=\"https://github.com/1amageek/Ballcap-iOS/blob/master/Ballcap.png\" width=\"100%\"\u003e\n\n [![Version](http://img.shields.io/cocoapods/v/Ballcap.svg)](http://cocoapods.org/?q=Pring)\n [![Platform](http://img.shields.io/cocoapods/p/Ballcap.svg)](http://cocoapods.org/?q=Pring)\n [![Downloads](https://img.shields.io/cocoapods/dt/Ballcap.svg?label=Total%20Downloads\u0026colorB=28B9FE)](https://cocoapods.org/pods/Ballcap)\n\nBallcap is a database schema design framework for Cloud Firestore.\n\n__Why Ballcap__\n\nCloud Firestore is a great schema-less and flexible database that can handle data. However, its flexibility can create many bugs in development. Ballcap can assign schemas to Cloud Firestore to visualize data structures. This plays a very important role when developing as a team.\n\nInspired by https://github.com/firebase/firebase-ios-sdk/tree/pb-codable3\n\n[Please donate to continue development.](https://gum.co/lNNIn)\n\n\u003cimg src=\"https://github.com/1amageek/pls_donate/blob/master/kyash.jpg\" width=\"180\"\u003e\n\n- Ballcap for TypeScript: https://github.com/1amageek/ballcap.ts\n\n__Sameple projects__\n\n- [Messagestore](https://github.com/1amageek/Messagestore)  Chat framework created by Cloud Firestore.\n\n### Feature\n\n☑️ Firestore's document schema with Swift Codable\u003cbr\u003e\n☑️ Of course type safety.\u003cbr\u003e\n☑️ It seamlessly works with Firestore and Storage.\u003cbr\u003e\n\n## Requirements ❗️\n- iOS 10 or later\n- Swift 5.0 or later\n- [Firebase firestore](https://firebase.google.com/docs/firestore/quickstart)\n- [Firebase storage](https://firebase.google.com/docs/storage/ios/start)\n\n## Installation ⚙\n#### [CocoaPods](https://github.com/cocoapods/cocoapods)\n\n- Insert `pod 'Ballcap' ` to your Podfile.\n- Run `pod install`.\n\nIf you have a Feature Request, please post an [issue](https://github.com/1amageek/Ballcap/issues/new).\n\n## Usage\n\n### Document scheme\n\nYou must conform to the Codable and Modelable protocols to define Scheme.\n\n```swift\nstruct Model: Codable, Equatable, Modelable {\n    var number: Int = 0\n    var string: String = \"Ballcap\"\n}\n```\n\n### Initialization\n\nThe document is initialized as follows:\n\n```swift\n\n// use auto id\nlet document: Document\u003cModel\u003e = Document()\n\nprint(document.data?.number) // 0\nprint(document.data?.string) // \"Ballcap\"\n\n// KeyPath\nprint(document[\\.number]) // 0\nprint(document[\\.string]) // \"Ballcap\"\n\n\n// Use your specified ID\nlet document: Document\u003cModel\u003e = Document(id: \"YOUR_ID\")\n\nprint(document.data?.number) // 0\nprint(document.data?.string) // \"Ballcap\"\n\n// KeyPath\nprint(document[\\.number]) // 0\nprint(document[\\.string]) // \"Ballcap\"\n\n\n// Use your specified DocumentReference\nlet documentReference: DocumentReference = Firestore.firestore().document(\"collection/document\")\n// note: If DocumentReference is specified, data is initialized with nil. \nlet document: Document\u003cModel\u003e = Document(documentReference) \n\nprint(document.data?.number) // nil\nprint(document.data?.string) // nil\n\n// KeyPath\nprint(document[\\.number]) // fail\nprint(document[\\.string]) // fail\n\n```\n\n### CRUD\n\nBallcap has a cache internally.When using the cache, use `Batch` instead of `WriteBatch`.\n\n```swift\n// save\ndocument.save()\n\n// update\ndocument.update()\n\n// delete\ndocument.delete()\n\n// Batch\nlet batch: Batch = Batch()\nbatch.save(document: document)\nbatch.update(document: document)\nbatch.delete(document: document)\nbatch.commit()\n```\n\nYou can get data by using the get function.\n\n```swift\nDocument\u003cModel\u003e.get(id: \"DOCUMENT_ID\", completion: { (document, error) in\n    print(document.data)\n})\n```\n\nThe next get function gets data in favor of the cache. If there is no cached data, it gets from the server.\n```swift\nlet document: Document\u003cModel\u003e = Document(\"DOCUMENT_ID\")\ndocument.get { (document, error) in\n   print(document.data)\n}\n```\n\n__Why data is optional?__\n\nIn CloudFirestore, DocumentReference does not necessarily have data. There are cases where there is no data under the following conditions.\n\n1. If no data is stored in DocumentReference.\n1. If data was acquired using `Source.cache` from DocumentReference, but there is no data in cache.\n\nBallcap recommends that developers unwrap if they can determine that there is data.\n\nIt is also possible to access the cache without using the network.\n\n```swift\nlet document: Document\u003cModel\u003e = Document(id: \"DOCUMENT_ID\")\nprint(document.cache?.number) // 0\nprint(document.cache?.string) // \"Ballcap\"\n```\n\n### Custom properties\n\nBallcap is preparing custom property to correspond to FieldValue.\n\n__ServerTimestamp__\n\nProperty for handling `FieldValue.serverTimestamp()`\n\n```swift\nstruct Model: Codable, Equatable {\n    let serverValue: ServerTimestamp\n    let localValue: ServerTimestamp\n}\nlet model = Model(serverValue: .pending,\n                  localValue: .resolved(Timestamp(seconds: 0, nanoseconds: 0)))\n```\n\n__IncrementableInt__ \u0026 __IncrementableDouble__\n\nProperty for handling `FieldValue.increment()`\n\n```swift\nstruct Model: Codable, Equatable, Modelable {\n    var num: IncrementableInt = 0\n}\nlet document: Document\u003cModel\u003e = Document()\ndocument.data?.num = .increment(1)\n```\n\n__OperableArray__\n\nProperty for handling `FieldValue.arrayRemove()`, `FieldValue.arrayUnion()`\n\n```swift\nstruct Model: Codable, Equatable, Modelable {\n    var array: OperableArray\u003cInt\u003e = [0, 0]\n}\nlet document: Document\u003cModel\u003e = Document()\ndocument.data?.array = .arrayUnion([1])\ndocument.data?.array = .arrayRemove([1])\n```\n\n### File\n\nFile is a class for accessing Firestorage.\nYou can save Data in the same path as Document by the follow:\n```swift\nlet document: Document\u003cModel\u003e = Document(id: \"DOCUMENT_ID\")\nlet file: File = File(document.storageReference)\n```\n\nFile supports multiple MIMETypes. Although File infers MIMEType from the name, it is better to input MIMEType explicitly.\n\n- [x] plain\n- [x] csv\n- [x] html\n- [x] css\n- [x] javascript\n- [x] octetStream(String?)\n- [x] pdf\n- [x] zip\n- [x] tar\n- [x] lzh\n- [x] jpeg\n- [x] pjpeg\n- [x] png\n- [x] gif\n- [x] mp4\n- [x] custom(String, String)\n\n#### Upload \u0026 Download\n\nUpload and Download each return a task. You can manage your progress by accessing tasks.\n\n```swift\n// upload\nlet ref: StorageReference = Storage.storage().reference().child(\"/a\")\nlet data: Data = \"test\".data(using: .utf8)!\nlet file: File = File(ref, data: data, name: \"n\", mimeType: .plain)\nlet task = file.save { (metadata, error) in\n    \n}\n\n// download\nlet task = file.getData(completion: { (data, error) in\n    let text: String = String(data: data!, encoding: .utf8)!\n})\n```\n\n#### StorageBatch\n\nStorageBatch is used when uploading multiple files to Cloud Storage.\n\n```swift\nlet textData: Data = \"test\".data(using: .utf8)!\nlet textFile: File = File(Storage.storage().reference(withPath: \"c\"), data: textData, mimeType: .plain)\nbatch.save(textFile)\n\nlet jpgData: Data = image.jpegData(compressionQuality: 1)!\nlet jpgFile: File = File(Storage.storage().reference(withPath: \"d\"), jpgData: textData, mimeType: .jpeg)\nbatch.save(jpgFile)\nbatch.commit { error in\n\n}\n```\n\n### DataSource\n\nBallcap provides a DataSource for easy handling of Collections and SubCollections.\n\n##### DataSource initialize\n\n__from Document__\n\n```swift\nlet dataSource: DataSource\u003cItem\u003e = Document\u003cItem\u003e.query.dataSource()\n```\n\n__from Collection Reference__\n\n#### CollectionReference\n```swift\nlet query: DataSource\u003cDocument\u003cItem\u003e\u003e.Query = DataSource.Query(Firestore.firestore().collection(\"items\"))\nlet dataSource = DataSource(reference: query)\n```\n\n#### CollectionGroup\n```swift\nlet query: DataSource\u003cDocument\u003cItem\u003e\u003e.Query = DataSource.Query(Firestore.firestore().collectionGroup(\"items\"))\nlet dataSource = DataSource(reference: query)\n```\n\n#### Your custom object\n\n```swift\n// from Custom class\nlet dataSource: DataSource\u003cItem\u003e = Item.query.dataSource()\n\n// from CollectionReference\nlet query: DataSource\u003cItem\u003e.Query = DataSource.Query(Item.collectionReference)\nlet dataSource: DataSource\u003cItem\u003e = query.dataSource()\n```\n\n__NSDiffableDataSourceSnapshot__\n\n```swift\nself.dataSource = Document\u003cItem\u003e.query\n    .order(by: \"updatedAt\", descending: true)\n    .limit(to: 3)\n    .dataSource()\n    .retrieve(from: { (snapshot, documentSnapshot, done) in\n        let document: Document\u003cItem\u003e = Document(documentSnapshot.reference)\n        document.get { (item, error) in\n            done(item!)\n        }\n    })\n    .onChanged({ (snapshot, dataSourceSnapshot) in\n        var snapshot: NSDiffableDataSourceSnapshot\u003cSection, DocumentProxy\u003cItem\u003e\u003e = self.tableViewDataSource.snapshot()\n        snapshot.appendItems(dataSourceSnapshot.changes.insertions.map { DocumentProxy(document: $0)})\n        snapshot.deleteItems(dataSourceSnapshot.changes.deletions.map { DocumentProxy(document: $0)})\n        snapshot.reloadItems(dataSourceSnapshot.changes.modifications.map { DocumentProxy(document: $0)})\n        self.tableViewDataSource.apply(snapshot, animatingDifferences: true)\n    })\n    .listen()\n```\n\n__UITableViewDelegate, UITableViewDataSource__\n\n```swift\nself.dataSource = Document\u003cItem\u003e.query\n    .order(by: \"updatedAt\", descending: true)\n    .limit(to: 3)\n    .dataSource()\n    .retrieve(from: { (snapshot, documentSnapshot, done) in\n        let document: Document\u003cItem\u003e = Document(documentSnapshot.reference)\n        document.get { (item, error) in\n            done(item!)\n        }\n    })\n    .onChanged({ (snapshot, dataSourceSnapshot) in\n        self.tableView.performBatchUpdates({\n            self.tableView.insertRows(at: dataSourceSnapshot.changes.insertions.map { IndexPath(item: dataSourceSnapshot.after.firstIndex(of: $0)!, section: 0)}, with: .automatic)\n            self.tableView.deleteRows(at: dataSourceSnapshot.changes.deletions.map { IndexPath(item: dataSourceSnapshot.before.firstIndex(of: $0)!, section: 0)}, with: .automatic)\n            self.tableView.reloadRows(at: dataSourceSnapshot.changes.modifications.map { IndexPath(item: dataSourceSnapshot.after.firstIndex(of: $0)!, section: 0)}, with: .automatic)\n        }, completion: nil)\n    })\n    .listen()\n```\n\n## Relationship between Document and Object\n\n`Document` is a `class` that inherits Object. For simple operations, it is enough to use `Document`.\n\n```swift\npublic final class Document\u003cModel: Modelable \u0026 Codable\u003e: Object, DataRepresentable, DataCacheable {\n\n    public var data: Model?\n    \n}\n```\n\nYou can perform complex operations by extending `Object` and defining your own class.\nUse examples are explained in [Using Ballcap with SwiftUI](https://github.com/1amageek/Ballcap-iOS#using-ballcap-with-swiftui)\n\n\n\n## Migrate from [Pring](https://github.com/1amageek/Pring)\n\n### Overview\nThe difference from Pring is that ReferenceCollection and NestedCollection have been abolished.\nIn Pring, adding a child Object to the ReferenceCollection and NestedCollection of the parent Object saved the parent Object at the same time when it was saved.\nBallcap requires the developer to save SubCollection using Batch.\nIn addition, Pring also saved the File at the same time as the Object with the File was saved.\nBallcap requires that developers save files using StorageBatch.\n\n### Scheme\nBallcap can handle Object class by inheriting Object class like Pring.\nIf you inherit Object class, you must conform to `DataRepresentable`.\n\n\n```swift\nclass Room: Object, DataRepresentable {\n\n    var data: Model?\n\n    struct Model: Modelable \u0026 Codable {\n        var members: [String] = []\n    }\n}\n```\n\n__SubCollection__\n\nBallcap has discontinued NestedCollection and ReferenceCollection Class. Instead, it represents SubCollection by defining CollectionKeys.\n\nClass must match `HierarchicalStructurable` to use CollectionKeys.\n```swift\nclass Room: Object, DataRepresentable \u0026 HierarchicalStructurable {\n\n    var data: Model?\n    \n    var transcripts: [Transcript] = []\n\n    struct Model: Modelable \u0026 Codable {\n        var members: [String] = []\n    }\n\n    enum CollectionKeys: String {\n        case transcripts\n    }\n}\n```\n\nUse the collection function to access the SubCollection.\n```swift\nlet collectionReference: CollectionReference = obj.collection(path: .transcripts)\n```\n\nSubCollection's Document save\n```swift\nlet batch: Batch = Batch()\nlet room: Room = Room()\nbatch.save(room.transcripts, to: room.collection(path: .transcripts))\nbatch.commit()\n```\n\n\n## Using Ballcap with SwiftUI\n\nFirst, create an object that conforms to `ObservableObject`.\n`DataListenable` makes an Object observable.\n\n```swift\nfinal class User: Object, DataRepresentable, DataListenable, ObservableObject, Identifiable {\n\n    typealias ID = String\n\n    override class var name: String { \"users\" }\n\n    struct Model: Codable, Modelable {\n\n        var name: String = \"\"\n    }\n\n    @Published var data: User.Model?\n\n    var listener: ListenerRegistration?\n}\n```\n\nNext, create a `View` that displays this object.\n\n\n```swift\nstruct UserView: View {\n\n    @ObservedObject var user: User\n\n    @State var isPresented: Bool = false\n\n    var body: some View {\n        VStack {\n            Text(user[\\.name])\n        }\n        .navigationBarTitle(Text(\"User\"))\n        .navigationBarItems(trailing: Button(\"Edit\") {\n            self.isPresented.toggle()\n        })\n        .sheet(isPresented: $isPresented) {\n            UserEditView(user: self.user.copy(), isPresented: self.$isPresented)\n        }\n        .onAppear {\n            _ = self.user.listen()\n        }\n    }\n}\n```\n\nYou can access the object data using `subscript`.\n\n```swift\nText(user[\\.name])\n```\n\nStart user observation with `onAppear`.\n\n```swift\n.onAppear {\n    _ = self.user.listen()\n}\n```\n\n### Copy object\n\nPass a copy of Object to EditView before editing the data.\n\n```swift\n.sheet(isPresented: $isPresented) {\n    UserEditView(user: self.user.copy(), isPresented: self.$isPresented)\n}\n```\n\nSince the Object is being observed by the listener, changes can be caught automatically.\n\nFinally, create a view that can update the object.\n\n```swift\nstruct UserEditView: View {\n\n    @ObservedObject var user: User\n\n    @Binding var isPresented: Bool\n\n    var body: some View {\n\n        VStack {\n\n            Form {\n                Section(header: Text(\"Name\")) {\n                    TextField(\"Name\", text: $user[\\.name])\n                }\n            }\n\n            Button(\"Save\") {\n                self.user.update()\n                self.isPresented.toggle()\n            }\n        }.frame(height: 200)\n    }\n}\n```\n\nUpdating an object is possible only with `update()`.\n\n```swift\nButton(\"Update\") {\n    self.user.update()\n    self.isPresented.toggle()\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2F1amageek%2Fballcap-ios","html_url":"https://awesome.ecosyste.ms/projects/github.com%2F1amageek%2Fballcap-ios","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2F1amageek%2Fballcap-ios/lists"}