{"id":16684421,"url":"https://github.com/sgr-ksmt/firesnapshot","last_synced_at":"2025-10-08T08:57:30.731Z","repository":{"id":35138071,"uuid":"211457149","full_name":"sgr-ksmt/FireSnapshot","owner":"sgr-ksmt","description":"A useful Firebase-Cloud-Firestore Wrapper with Codable.","archived":false,"fork":false,"pushed_at":"2022-07-22T04:10:57.000Z","size":229,"stargazers_count":56,"open_issues_count":8,"forks_count":9,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-09-13T06:35:38.546Z","etag":null,"topics":["cloudfirestore","codable","decode","encode","firebase","firestore","ios","model","modelframework","oss","propertywr","simple","snapshot","swift","swift5","type-safe"],"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/sgr-ksmt.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},"funding":{"github":["sgr-ksmt"]}},"created_at":"2019-09-28T06:46:38.000Z","updated_at":"2024-01-20T09:37:30.000Z","dependencies_parsed_at":"2022-08-08T05:16:05.039Z","dependency_job_id":null,"html_url":"https://github.com/sgr-ksmt/FireSnapshot","commit_stats":null,"previous_names":[],"tags_count":18,"template":false,"template_full_name":null,"purl":"pkg:github/sgr-ksmt/FireSnapshot","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sgr-ksmt%2FFireSnapshot","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sgr-ksmt%2FFireSnapshot/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sgr-ksmt%2FFireSnapshot/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sgr-ksmt%2FFireSnapshot/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sgr-ksmt","download_url":"https://codeload.github.com/sgr-ksmt/FireSnapshot/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sgr-ksmt%2FFireSnapshot/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":278916432,"owners_count":26068090,"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","status":"online","status_checked_at":"2025-10-08T02:00:06.501Z","response_time":56,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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","codable","decode","encode","firebase","firestore","ios","model","modelframework","oss","propertywr","simple","snapshot","swift","swift5","type-safe"],"created_at":"2024-10-12T14:43:45.733Z","updated_at":"2025-10-08T08:57:30.707Z","avatar_url":"https://github.com/sgr-ksmt.png","language":"Swift","funding_links":["https://github.com/sponsors/sgr-ksmt"],"categories":[],"sub_categories":[],"readme":"\u003cp align='center'\u003e\n\n\u003cimg src='https://raw.githubusercontent.com/sgr-ksmt/FireSnapshot/master/assets/logo.png' width='600px' /\u003e\n\n\u003c/p\u003e\n\n\u003cdiv align='center'\u003e\n\n[![Release](https://img.shields.io/github/release/sgr-ksmt/FireSnapshot.svg?style=for-the-badge)](https://github.com/sgr-ksmt/FireSnapshot/releases)\n![Swift](https://img.shields.io/badge/swift-5.1-orange.svg?style=for-the-badge)\n![Firebase](https://img.shields.io/badge/firebase-6.11.0-orange.svg?style=for-the-badge)\n![Platform](https://img.shields.io/badge/Platform-iOS-blue.svg?style=for-the-badge)\n[![license](https://img.shields.io/github/license/sgr-ksmt/FireSnapshot.svg?style=for-the-badge)](https://github.com/sgr-ksmt/FireSnapshot/blob/master/LICENSE)\n\n**A useful Firebase-Cloud-Firestore Wrapper with Codable.**  \n\nDeveloped by [@sgr-ksmt](https://github.com/sgr-ksmt) [![Twitter Follow](https://img.shields.io/twitter/follow/_sgr_ksmt?label=Follow\u0026style=social)](https://twitter.com/_sgr_ksmt)\n\u003c/div\u003e\n\n\u003chr /\u003e\n\n## Table of Contents \u003c!-- omit in toc --\u003e\n- [Feature](#feature)\n- [Usage](#usage)\n  - [Basic Usage](#basic-usage)\n  - [Advanced Usage](#advanced-usage)\n- [Installation](#installation)\n- [Dependencies](#dependencies)\n- [Road to 1.0](#road-to-10)\n- [Development](#development)\n- [Communication](#communication)\n- [Credit](#credit)\n- [License](#license)\n\n\u003chr /\u003e\n\n## Feature\n\n- 🙌 Support Codable (Use [`FirebaseFirestoreSwift`](https://github.com/firebase/firebase-ios-sdk/tree/master/Firestore/Swift) inside).\n- 🙌 Provide easy-to-use methods for CRUD, Batch, Transaction.\n- 🙌 Support `array-union/array-remove`.\n- 🙌 Support `FieldValue.increment`.\n- 🙌 Support `FieldValue.delete()`.\n- 🙌 Support KeyPath based query.\n\n### Use Swift features(version: 5.1) \u003c!-- omit in toc --\u003e\n\n- 💪 **`@propertyWrapper`**: [SE-0258](https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md)\n- 💪 **Key Path Member Lookup**: [SE-0252](https://github.com/apple/swift-evolution/blob/master/proposals/0252-keypath-dynamic-member-lookup.md)\n\n\u003chr /\u003e\n\n## Usage\n\n- 👉 [Example App](https://github.com/sgr-ksmt/FireTodo)\n\n### Basic Usage\n\nThe type of `Document` must be conformed to the `SnapshotData` protocol.  \n`SnapshotData` protocol inherits `Codable`.  For example: \n\n```swift\nstruct Product: SnapshotData {\n    var name: String = \"\"\n    var desc: String?\n    var price: Int = 0\n    var attributes: [String: String] = [:]\n}\n```\n\nIt is convenient to define `DocumentPath\u003cT\u003e` and `CollectionPath\u003cT\u003e`.  \nDefine path for extension of `DocumentPaths` or `CollectionPaths`.\n\n\n```swift\nextension CollectionPaths {\n    static let products = CollectionPath\u003cProduct\u003e(\"products\")\n}\n\nextension DocumentPaths {\n    static func product(_ productID: String) -\u003e DocumentPath\u003cProduct\u003e {\n        CollectionPaths.products.document(productID)\n    }\n}\n```\n\nCreate `Snapshot` with model that comformed to `SnapshotData` and path.\n\n```swift\nlet product = Snapshot\u003cProduct\u003e(data: Product(), path: CollectionPath.products)\n```\n\nIn short 👇\n\n```swift\nlet product = Snapshot(data: .init(), path: .products)\n```\n\nYou can save it by calling `create(completion:)`\n\n```swift\nproduct.create { error in\n    if let error = error {\n        print(\"error\", error)\n        return\n    }\n    print(\"created!\")\n}\n```\n\n`FireSnapshot` also provides read(`get document(s)/listen document(s)`), write(`update/delete`), write with batch and transaction\n\n```swift\n// Update document\nproduct.update { error in\n    if let error = error {\n        print(\"error\", error)\n        return\n    }\n    print(\"updated!\")\n}\n\n// Delete document\nproduct.delete { error in\n    if let error = error {\n        print(\"error\", error)\n        return\n    }\n    print(\"deleted!\")\n}\n\n// Get document\nSnapshot.get(.product(\"some_product_id\")) { result in\n    switch result {\n    case let .success(product):\n        print(product.name)\n    case let .failure(error):\n        print(error)\n    }\n}\n\n// Listen document\nlet listener = Snapshot.listen(.product(\"some_product_id\")) { result in\n    switch result {\n    case let .success(product):\n        print(\"listened new product\", product.name)\n    case let .failure(error):\n        print(error)\n    }\n}\n\n// Get documents\nSnapshot.get(.products) { result in\n    switch result {\n    case let .success(products):\n        print(products.count)\n    case let .failure(error):\n        print(error)\n    }\n}\n\n// Listen documents\nlet listener = Snapshot.listen(.products) { result in\n    switch result {\n    case let .success(products):\n        print(\"listened new products\", products.count)\n    case let .failure(error):\n        print(error)\n    }\n}\n```\n\nIf you can read/write timestamp such as `createTime` and `updateTime`, model must be conform to `HasTimestamps` protocol.\n\n```swift\nstruct Product: SnapshotData, HasTimestamps {\n    var name: String = \"\"\n    var desc: String?\n    var price: Int = 0\n    var attributes: [String: String] = [:]\n}\n\nlet product = Snapshot(data: .init(), path: .products)\n// `createTime` and `updateTime` will be written to field with other properties.\nproduct.create()\n\nSnapshot.get(product.path) { result in\n    guard let p = try? result.get() else {\n        return\n    }\n\n    // optional timestamp value.\n    print(p.createTime)\n    print(p.updateTime)\n\n    // `updateTime` will be updated with other properties.\n    p.update()\n}\n```\n\n\u003chr /\u003e\n\n### Advanced Usage\n\n#### `@IncrementableInt` / `@IncrementableDouble` \u003c!-- omit in toc --\u003e\n\nIf you want to use `FieldValue.increment` on model, use `@IncrementableInt(Double)`.  \n\n- The type of `@IncrementableInt` property is `Int64`.  \n- The type of `@IncrementableDouble` property is `Double`.  \n\n```swift\nextension CollectionPaths {\n    static let products = CollectionPath\u003cModel\u003e(\"models\")\n}\n\nstruct Model: SnapshotData {\n    @IncrementableInt var count = 10\n    @IncrementableDouble var distance = 10.0\n}\n\nSnapshot.get(.model(modelID)) { result in\n    guard let model = try? result.get() else {\n        return\n    }\n    // Refer a number\n    print(model.count) // print `10`.\n    print(model.distance) // print `10.0`.\n\n    // Increment (use `$` prefix)\n    model.$count.increment(1)\n    print(model.count) // print `11`.\n    model.update()\n\n    model.$distance.increment(1.0)\n    print(model.distance) // print `11.0`.\n    model.update()\n\n    // Decrement\n    model.$count.increment(-1)\n    print(model.count) // print `9`.\n    model.update()\n\n    model.$distance.increment(-1.0)\n    print(model.distance) // print `9.0`.\n    model.update()\n\n    // if you want to reset property, use `reset` method.\n    model.$count.reset()\n}\n```\n\n#### `@AtomicArray` \u003c!-- omit in toc --\u003e\n\nIf you want to use `FieldValue.arrayUnion` or `FieldValue.arrayRemove`, use `@AtomicArray`.  \n\nThe type of `@AtomicArray`'s element must be conformed to `Codable` protocol.\n\n```swift\nextension CollectionPaths {\n    static let products = CollectionPath\u003cModel\u003e(\"models\")\n}\n\nstruct Model: SnapshotData {\n    @AtomicArray var languages: [String] = [\"en\", \"ja\"]\n}\n\nSnapshot.get(.model(modelID)) { result in\n    guard let model = try? result.get() else {\n        return\n    }\n\n    // Refer an array\n    print(model.languages) // print `[\"en\", \"ja\"]`.\n\n    // Union element(s)\n    model.$languages.union(\"zh\")\n    print(model.count) // print `[\"en\", \"ja\", \"zh\"]`.\n    model.update()\n\n    // Remove element(s)\n    model.$languages.remove(\"en\")\n    print(model.count) // print `[\"ja\"]`.\n    model.update()\n\n    // if you want to reset property, use `reset` method.\n    model.$languages.reset()\n}\n```\n\n#### `@DeletableField` \u003c!-- omit in toc --\u003e\n\nIF you want to use `FieldValue.delete`, use `@DeletableField`.\n\n```swift\nextension CollectionPaths {\n    static let products = CollectionPath\u003cModel\u003e(\"models\")\n}\n\nstruct Model: SnapshotData {\n    var bio: DeletableField\u003cString\u003e? = .init(value: \"I'm a software engineer.\")\n}\n\nSnapshot.get(.model(modelID)) { result in\n    guard let model = try? result.get() else {\n        return\n    }\n\n    print(model.bio?.value) // print `Optional(\"I'm a software engineer.\")`\n\n    // Delete property\n    model.bio.delete()\n    model.update()\n}\n\n// After updated\nSnapshot.get(.model(modelID)) { result in\n    guard let model = try? result.get() else {\n        return\n    }\n\n    print(model.bio) // nil\n    print(model.bio?.value) // nil\n}\n```\n\n**NOTE:** \nNormally, when property is set to nil, `{key: null}` will be written to document,  \nbut when using `FieldValue.delete`, field of `key` will be deleted from document.\n\n#### KeyPath-based query \u003c!-- omit in toc --\u003e\n\nYou can use KeyPath-based query generator called `QueryBuilder` if the model conform to `FieldNameReferable` protocol.\n\n```swift\nextension CollectionPaths {\n    static let products = CollectionPath\u003cProduct\u003e(\"products\")\n}\n\nstruct Product: SnapshotData, HasTimestamps {\n    var name: String = \"\"\n    var desc: String?\n    var price: Int = 0\n    var deleted: Bool = false\n    var attributes: [String: String] = [:]\n}\n\nextension Product: FieldNameReferable {\n    static var fieldNames: [PartialKeyPath\u003cMock\u003e : String] {\n        return [\n            \\Self.self.name: \"name\",\n            \\Self.self.desc: \"desc\",\n            \\Self.self.price: \"price\",\n            \\Self.self.deleted: \"deleted\",\n        ]\n    }\n}\n\nSnapshot.get(.products, queryBuilder: { builder in\n    builder\n        .where(\\.price, isGreaterThan: 5000)\n        .where(\\.deleted, isEqualTo: false)\n        .order(by: \\.updateTime, descending: true)\n}) { result in\n    ...\n}\n```\n\n\u003chr /\u003e\n\n## Installation\n\n- CocoaPods\n\n```ruby\npod 'FireSnapshot', '~\u003e 0.11.1'\n```\n\n\u003chr /\u003e\n\n## Dependencies\n\n- Firebase: `v6.12.0` or higher.\n- FirebaseFirestoreSwift: Fetch from master branch.\n- Swift: `5.1` or higher.\n\n\u003chr /\u003e\n\n## Road to 1.0\n\n- Until 1.0 is reached, minor versions will be breaking 🙇‍.\n\n\n\u003chr /\u003e\n\n## Development\n\n### Setup \u003c!-- omit in toc --\u003e\n\n```sh\n$ git clone ...\n$ cd path/to/FireSnapshot\n$ make\n$ open FireSnapshot.xcworkspace\n```\n\n### Unit Test \u003c!-- omit in toc --\u003e\n\nStart `Firestore Emulator` before running Unit Test.\n\n```sh\n$ npm install -g firebase-tools\n$ firebase setup:emulators:firestore\n$ cd ./firebase/\n$ firebase emulators:start --only firestore\n# Open Xcode and run Unit Test after running emulator.\n```\n\nor, run `./scripts/test.sh`.\n\n\u003chr /\u003e\n\n## Communication\n\n- If you found a bug, open an issue.\n- If you have a feature request, open an issue.\n- If you want to contribute, submit a pull request.:muscle:\n\n\u003chr /\u003e\n\n## Credit\n\nFireSnapshot was inspired by followings:\n\n- [starhoshi/tart](https://github.com/starhoshi/tart)\n- [alickbass/CodableFirebase](https://github.com/alickbass/CodableFirebase)\n\n\u003chr /\u003e\n\n## License\n\n**FireSnapshot** is under MIT license. See the [LICENSE](LICENSE) file for more info.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsgr-ksmt%2Ffiresnapshot","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsgr-ksmt%2Ffiresnapshot","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsgr-ksmt%2Ffiresnapshot/lists"}