Code Monkey home page Code Monkey logo

ballcap-ios's Introduction

🧢 Ballcap-iOS

Version Platform Downloads

Ballcap is a database schema design framework for Cloud Firestore.

Why Ballcap

Cloud 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.

Inspired by https://github.com/firebase/firebase-ios-sdk/tree/pb-codable3

Please donate to continue development.

Sameple projects

Feature

☑️ Firestore's document schema with Swift Codable
☑️ Of course type safety.
☑️ It seamlessly works with Firestore and Storage.

Requirements ❗️

Installation ⚙

  • Insert pod 'Ballcap' to your Podfile.
  • Run pod install.

If you have a Feature Request, please post an issue.

Usage

Document scheme

You must conform to the Codable and Modelable protocols to define Scheme.

struct Model: Codable, Equatable, Modelable {
    var number: Int = 0
    var string: String = "Ballcap"
}

Initialization

The document is initialized as follows:

// 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 has a cache internally.When using the cache, use Batch instead of 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()

You can get data by using the get function.

Document<Model>.get(id: "DOCUMENT_ID", completion: { (document, error) in
    print(document.data)
})

The next get function gets data in favor of the cache. If there is no cached data, it gets from the server.

let document: Document<Model> = Document("DOCUMENT_ID")
document.get { (document, error) in
   print(document.data)
}

Why data is optional?

In CloudFirestore, DocumentReference does not necessarily have data. There are cases where there is no data under the following conditions.

  1. If no data is stored in DocumentReference.
  2. If data was acquired using Source.cache from DocumentReference, but there is no data in cache.

Ballcap recommends that developers unwrap if they can determine that there is data.

It is also possible to access the cache without using the network.

let document: Document<Model> = Document(id: "DOCUMENT_ID")
print(document.cache?.number) // 0
print(document.cache?.string) // "Ballcap"

Custom properties

Ballcap is preparing custom property to correspond to FieldValue.

ServerTimestamp

Property for handling 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

Property for handling FieldValue.increment()

struct Model: Codable, Equatable, Modelable {
    var num: IncrementableInt = 0
}
let document: Document<Model> = Document()
document.data?.num = .increment(1)

OperableArray

Property for handling 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])

File

File is a class for accessing Firestorage. You can save Data in the same path as Document by the follow:

let document: Document<Model> = Document(id: "DOCUMENT_ID")
let file: File = File(document.storageReference)

File supports multiple MIMETypes. Although File infers MIMEType from the name, it is better to input MIMEType explicitly.

  • plain
  • csv
  • html
  • css
  • javascript
  • octetStream(String?)
  • pdf
  • zip
  • tar
  • lzh
  • jpeg
  • pjpeg
  • png
  • gif
  • mp4
  • custom(String, String)

Upload & Download

Upload and Download each return a task. You can manage your progress by accessing tasks.

// 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 is used when uploading multiple files to Cloud Storage.

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

Ballcap provides a DataSource for easy handling of Collections and SubCollections.

DataSource initialize

from Document

let dataSource: DataSource<Item> = Document<Item>.query.dataSource()

from Collection Reference

CollectionReference

let query: DataSource<Document<Item>>.Query = DataSource.Query(Firestore.firestore().collection("items"))
let dataSource = DataSource(reference: query)

CollectionGroup

let query: DataSource<Document<Item>>.Query = DataSource.Query(Firestore.firestore().collectionGroup("items"))
let dataSource = DataSource(reference: query)

Your custom object

// 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()

Relationship between Document and Object

Document is a class that inherits Object. For simple operations, it is enough to use Document.

public final class Document<Model: Modelable & Codable>: Object, DataRepresentable, DataCacheable {

    public var data: Model?
    
}

You can perform complex operations by extending Object and defining your own class. Use examples are explained in Using Ballcap with SwiftUI

Migrate from Pring

Overview

The difference from Pring is that ReferenceCollection and NestedCollection have been abolished. In 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. Ballcap requires the developer to save SubCollection using Batch. In addition, Pring also saved the File at the same time as the Object with the File was saved. Ballcap requires that developers save files using StorageBatch.

Scheme

Ballcap can handle Object class by inheriting Object class like Pring. If you inherit Object class, you must conform to DataRepresentable.

class Room: Object, DataRepresentable {

    var data: Model?

    struct Model: Modelable & Codable {
        var members: [String] = []
    }
}

SubCollection

Ballcap has discontinued NestedCollection and ReferenceCollection Class. Instead, it represents SubCollection by defining CollectionKeys.

Class must match HierarchicalStructurable to use CollectionKeys.

class Room: Object, DataRepresentable & HierarchicalStructurable {

    var data: Model?
    
    var transcripts: [Transcript] = []

    struct Model: Modelable & Codable {
        var members: [String] = []
    }

    enum CollectionKeys: String {
        case transcripts
    }
}

Use the collection function to access the SubCollection.

let collectionReference: CollectionReference = obj.collection(path: .transcripts)

SubCollection's Document save

let batch: Batch = Batch()
let room: Room = Room()
batch.save(room.transcripts, to: room.collection(path: .transcripts))
batch.commit()

Using Ballcap with SwiftUI

First, create an object that conforms to ObservableObject. DataListenable makes an Object observable.

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?
}

Next, create a View that displays this object.

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()
        }
    }
}

You can access the object data using subscript.

Text(user[\.name])

Start user observation with onAppear.

.onAppear {
    _ = self.user.listen()
}

Copy object

Pass a copy of Object to EditView before editing the data.

.sheet(isPresented: $isPresented) {
    UserEditView(user: self.user.copy(), isPresented: self.$isPresented)
}

Since the Object is being observed by the listener, changes can be caught automatically.

Finally, create a view that can update the object.

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)
    }
}

Updating an object is possible only with update().

Button("Update") {
    self.user.update()
    self.isPresented.toggle()
}

ballcap-ios's People

Contributors

1amageek avatar brightsider avatar giiiita avatar imaizume avatar kinergy avatar naturalui avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

ballcap-ios's Issues

Allow initialization of Object subclasses with models

Is your feature request related to a problem? Please describe.
Model's of Object subclasses require empty init methods:

class User: Object, DataRepresentable {
    var data: Model?
    
    struct Model: Modelable, Codable {
        let id: String
        let firstName: String
        let lastName: String
        
        init() {
            self.id = ""
            self.firstName = ""
            self.lastName = ""
        }
        
        init(id: String, firstName: String, lastName: String) {
            self.id = id
            self.firstName = firstName
            self.lastName = lastName
        }
    }
}

And when used require doing a little dance with initializing separately and assigning:

let user = User()
let model = User.Model(id: userId,
                           firstName: firstName,
                           lastName: lastName)
user.data = model
user.save()

This is un-ideal as you need to write so much repetitive boilerplate for every Object subclass, and you can forget to assign the model to object.data.

Describe the solution you'd like
When initializing an instance of a subclass of Object it would be great if we required the data model to be provided along with it:

let model = User.Model(id: userId,
                           firstName: firstName,
                           lastName: lastName)
let user = User(data: model)
user.save()

Which would enable us to simplify the model blueprint to:

class User: Object, DataRepresentable {
    var data: Model?
    
    struct Model: Modelable, Codable {
        let id: String
        let firstName: String
        let lastName: String
    }
}

Describe alternatives you've considered
I listed the alternative route I'm using above.

Additional context
I'd be happy to help develop this if you think it would be a worthwhile feature.

A way to avoid Firestore crash

Hello. I'll be very happy if you could help me with avoiding firebase crash in a module!

I'm trying to make a framework that wraps Firestore SDK, but it crashes.
According to some comments on GitHub, using Firebase from multiple modules have some difficulties.

Firebase is a linked statically and can only be linked once into an app. It cannot be linked into multiple dynamic frameworks.

from firebase/firebase-ios-sdk#4263 (comment)

My crash seems to be the same as this issue: firebase/firebase-ios-sdk#4271

I'm interested in how Ballcap-iOS avoid this problem since it wraps Firestore in a module.
Do you know how Ballcap-iOS avoids this problem?

I've uploaded my sample app which has the same crash. It crashes by executing tests.
https://github.com/yoching/FirestoreDependencyExperiment

Thank you!

documentReference issue

Why this is not referenced to path?

static var documentReference: DocumentReference {
return Firestore.firestore().document("version/(self.modelVersion)")
}

WithSwiftUI Sample Crash

Describe the bug

WithSwiftUI example crashing on run.

[REQUIRED] Step 1: Describe your environment

  • Xcode version: Version 11.3 (11C29)
  • Ballcap version: master

[REQUIRED] Step 3: Describe the problem

Example not compiling

Steps to reproduce:

Clone repo
Change directory to example/SwiftUI
pod install
Copy over GoogleService-Info.plist (works on other projects)

Error Message

2020-01-15 02:51:53.892641-0800 WithSwiftUI[3613:4281424]  - <AppMeasurement>[I-ACS036002] Analytics screen reporting is enabled. Call +[FIRAnalytics setScreenName:setScreenClass:] to set the screen name or override the default screen class name. To disable screen reporting, set the flag FirebaseScreenReportingEnabled to NO (boolean) in the Info.plist
2020-01-15 02:51:53.958240-0800 WithSwiftUI[3613:4281430] 6.11.0 - [Firebase/Analytics][I-ACS023007] Analytics v.60103000 started
2020-01-15 02:51:53.958820-0800 WithSwiftUI[3613:4281430] 6.11.0 - [Firebase/Analytics][I-ACS023008] To enable debug logging set the following application argument: -FIRAnalyticsDebugEnabled (see http://goo.gl/RfcP7r)
keyNotFound(CodingKeys(stringValue: "name", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: \"name\", intValue: nil) (\"name\").", underlyingError: nil))
Fatal error: Unexpectedly found nil while unwrapping an Optional value: file /path/to/Ballcap-iOS/examples/WithSwiftUI/Pods/Ballcap/Ballcap/DataSource.swift, line 191
2020-01-15 02:51:54.117454-0800 WithSwiftUI[3613:4281428] Fatal error: Unexpectedly found nil while unwrapping an Optional value: file /path/to/Ballcap-iOS/examples/WithSwiftUI/Pods/Ballcap/Ballcap/DataSource.swift, line 191

How to remove document<>

image

print(Document<User>.modelName) // user

I'm a Pring user. I appreciate it.

ModelName is user, but collection name is document<user>. In case of Pring, collection name is user. Is this a Ballcap's feature? Can I change collection name without overriding Document class?

Is it possible to remove a field from a document

I'm using Ballcap in a SwiftUI environment and I have a class that contains an optional field of type DocumentReference in its Model.
Initially the field does not exist but I can set it through the UI of my app. This works just as expected.
But I would like to be able to also remove the reference. I tried setting it to nil but this doesn't work unfortunately.

Is there currently any way to achieve this with Ballcap?

Thanks for your help!

configure is inaccessible

Describe the bug

BallcapApp.configure showing the message "inaccessible due to internal protection level message".

[REQUIRED] Step 1: Describe your environment

  • Xcode version: 10.2.1
  • Bellcap version: 0.6.0

[REQUIRED] Step 3: Describe the problem

Steps to reproduce:

Put BallcapApp.configure in a class and after try to compile.

Relevant Code:

BallcapApp.configure(Firestore.firestore().document("version/1"))

Object' does not conform to protocol 'Modelable'

Describe the bug

A clear and concise description of what the bug is.

[REQUIRED] Step 1: Describe your environment

  • Xcode version: _____
  • Pring version: _____

[REQUIRED] Step 3: Describe the problem

Steps to reproduce:

What happened? How can we make the problem occur?
This could be a description, log/console output, etc.

Relevant Code:

// TODO(you): code here to reproduce the problem

FIRQuerySnapshotBlock is unavailable in Swift

I just made a pod update and am now using Firebase 10.1.0 and Ballcap 1.3.5 but when trying to build my app I get build errors inside Ballcaps Query.swift in lines 119 - 125.

        public func listen(includeMetadataChanges: Bool = true, listener: @escaping FIRQuerySnapshotBlock) -> ListenerRegistration {
            return query.addSnapshotListener(includeMetadataChanges: includeMetadataChanges, listener: listener)
        }

        public func get(completion: @escaping FIRQuerySnapshotBlock) {
            query.getDocuments(completion: completion)
        }

The error is for both functions

'FIRQuerySnapshotBlock' is unavailable in Swift: Use Swift's closure syntax instead.

Does anyone else have the problem?

I'm using:

  • Xcode 14.0.1
  • Cocoapods 1.11.3
  • Firebase 10.1.0
  • Ballcap 1.3.5

firebase SDKのバージョンアップ

いつもお世話になっております。

firebase SDKは現在v10.21.0が公開されていますが、こちら対応されるご予定はございますでしょうか?

というのもプロジェクトのfirebase SDKのバージョンを上げたいと考えていたところ、Ballcapが使用しているfirebaseのバージョンに引っ張られて更新できないためそのご確認となります。

DataSource updates not working for last deleted document in collection

Describe the bug

When deleting last document in collection, DataSource not picking up changes and do not notify about removed items.

[REQUIRED] Step 1: Describe your environment

  • Xcode version: 10.3 * Ballcap version: 0.10.4

[REQUIRED] Step 3: Describe the problem

If last document in collection deleted snapshot is not processed. Issue caused by this line.

guard let lastSnapshot = snapshot.documents.last else {

When deleting last document lastSnapshot is nil and self._execute(snapshot: snapshot) not getting called.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.