Skip to content
This repository was archived by the owner on Jan 22, 2023. It is now read-only.

Repository files navigation

VersionPlatformDownloads

Please donate to continue development.

https://github.com/1amageek/pring.ts

Pring <β>

Firestore model framework. The concept of Document and Collection has been added to Firestore. Pring defines the Scheme of the Document and enables type - safe programming. SubCollection can also be defined in Scheme.

Deep Dive into the Firebase

Please report issues here

Requirements ❗️

Installation ⚙

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

Feature 🎊

☑️ You can define Firestore's Document scheme.
☑️ Of course type safety.
☑️ It seamlessly works with Firestore and Storage.
☑️ You can easily associate subcollections.
☑️ Support GeoPoint.

Design 💻

Firestore Database Design

If you are going to use Firestore and make products, I recommend you to read it.

TODO ✅

Implementation

  • Implement DataType that Firestore can handle
  • Implement data management
  • Implement custom DataType (Specification under consideration)
  • Implement linkage with Firestorage
  • Implement the NestedCollection feature
  • Implement the ReferenceCollection feature
  • Implement DataSource
  • Implement Query-enabled DataSource (Specification under consideration)

Verification (Running Unit test)

  • Verify the implementation of DataType that Firestore can handle
  • Verify the implementation of data management
  • Verify the implementation of custom DataType
  • Verify cooperation with Firestorage
  • Verify the implementation of the NestedCollection feature
  • Verify the implementation of the ReferenceCollection feature
  • Verify the implementation of Query-enabled DataSource

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

Usage

For example..

@objcMembersclassUser:Object{@objcenumUserType:Int{case normal
case gold
case premium }dynamicvartype:UserType=.normal
dynamicvarname:String?dynamicvarthumbnail:File?dynamicvarfollowers:ReferenceCollection<User>=[]dynamicvaritems:NestedCollection<Item>=[]
// Custom property
overridefunc encode(_ key:String, value:Any?)->Any?{if key =="type"{returnself.type.rawValue
}returnnil}overridefunc decode(_ key:String, value:Any?)->Bool{if key =="type"{self.type =UserType(rawValue: value as!Int)returntrue}returnfalse}}
@objcMembersclassItem:Object{dynamicvarthumbnail:File?dynamicvarname:String?="OWABIISHI"}
// Set an arbitrary ID
letuser:User=User(id:"ID")
user.save()
letuserA:User=User()
userA.name ="userA"
userA.thumbnail =File(data:UIImageJPEGRepresentation(IMAGE,0.3)!, mimeType:.jpeg)letuserB:User=User()
userB.name ="userB"
userB.thumbnail =File(data:UIImageJPEGRepresentation(IMAGE,0.3)!, mimeType:.jpeg)letitem:Item=Item()
item.thumbnail =File(data:UIImageJPEGRepresentation(IMAGE,0.3)!, mimeType:.jpeg)
userA.followers.insert(userB)
userA.items.insert(item)
userA.save()

Important❗️

Pring clearly separates save and update. This is to prevent unexpected overwriting. Pring provides three methods of initializing Object.

Initialization giving AutoID to Object

letuser:User=User()

Initialization giving arbitrary ID

letuser:User=User(id:"YOUR_ID") // isSaved false

Initialization when dealing with already saved Object

If you are dealing with an Object that has already been saved, please perform the following initialization. In case of this initialization can not save Please update.

letuser:User=User(id:"YOUR_ID", value:[:]) // isSaved true

It is the developer's responsibility to manage the saved state of the Object.

Scheme

Pring inherits Object class and defines the Model. Pring supports many data types.

@objcMembersclassUser:Object{dynamicvararray:[String]=["array"]dynamicvarset:Set<String>=["set"]dynamicvarbool:Bool=truedynamicvarbinary:Data="data".data(using:.utf8)!
dynamicvarfile:File=File(data:UIImageJPEGRepresentation(UIImage(named:"")!,1))dynamicvarurl:URL=URL(string:"https://firebase.google.com/")!
dynamicvarint:Int=Int.max
dynamicvarfloat:Double=Double.infinity
dynamicvardate:Date=Date(timeIntervalSince1970:100)dynamicvargeoPoint:GeoPoint=GeoPoint(latitude:0, longitude:0)dynamicvarlist:List<Group>=[]dynamicvardictionary:[String:Any]=["key":"value"]dynamicvarstring:String="string"letgroup:Reference<Group>=.init()letnestedCollection:NestedCollection<Item>=[]letreferenceCollection:ReferenceCollection<User>=[]}
DataTypeDescription
ArrayIt is Array type.
SetIt is Set type.In Firestore it is expressed as {"value": true}.
BoolIt is a boolean value.
FileIt is File type. You can save large data files.
URLIt is URL type. It is saved as string in Firestore.
IntIt is Int type.
FloatIt is Float type. In iOS, it will be a 64 bit Double type.
DateIt is Date type.
GeoPointIt is GeoPoint type.
ListIt is Object array type.
DictionaryIt is a Dictionary type. Save the structural data.
nestedCollection or referenceCollectionIt is SubCollection type.
StringIt is String type.
ReferenceIt is Reference type. It hold DocumentReference
NullIt is Null type.
AnyIt is custom type. You can specify it as a custom type if it is a class that inherits from NSObject.

⚠️BoolIntFloatDouble are not supported optional type.

⚙️ Manage data

Save

Document can be saved only once.

letobject:MyObject=MyObject()
object.save{(ref, error)in
// completion
}

Retrieve

Retrieve document with ID.

MyObject.get(document!.id, block:{(document, error)in
// do something
})

Update

Document has an update method. Be careful as it is different from Salada.

MyObject.get(document!.id, block:{(document, error)in
document.string ="newString"
document.update{ error in
// update
}})

Delete

Delete document with ID.

MyObject.get(document!.id, block:{(document, error)in
document.delete()})

Batched writes

letbatch:WriteBatch=Firestore.firestore().batch()
batch.add(.save, object: userA) // ** File is not saved.
batch.add(.update, object: userB)
batch.add(.delete, object: userC)
batch.commit(completion:{(error)in
// error handling
})

List

List can access the Object faster than NestedCollection. List holds data in Document, not SubCollection.

// save
letorder:Order=Order()do{letorderItem:OrderItem=OrderItem()
orderItem.name ="aaaa"
orderItem.price =39
order.items.append(orderItem)}do{letorderItem:OrderItem=OrderItem()
orderItem.name ="bbb"
orderItem.price =21
order.items.append(orderItem)}
order.save()

Be sure to update the parent's object when updating data.

// update
Order.get("ORDER_ID"){(order, error)in
order.items.first.name ="hoge"
order.update()}

📄 File

Pring has a File class because it seamlessly works with Firebase Storage.

Save

File is saved with Document Save at the same time.

letobject:MyObject=MyObject()
object.thumbnailImage =File(data: PNG_DATA, mimeType:.png)lettasks:[String:StorageUploadTask]= object.save{(ref, error)in}

save method returns the StorageUploadTask that is set with the key. For details on how to use StorageUploadTask, refer to Firebase docs.

lettask:StorageUploadTask=tasks["thumbnailImage"]

Get data

Get data with size.

lettask:StorageDownloadTask= object.thumbnail.getData(100000, block:{(data, error)in
// do something
})

Update

If the Document is already saved, please use update method. update method also returns StorageUploadTask. Running update method automatically deletes old files.

letnewFile:File=File(data: PNG_DATA, mimeType:.png)
object.thumbnailImage = newFile
object.update()

Delete

Delete it with delete method.

object.thumbnailImage =File.delete()
object.update()

If it is held in an array, automatic file deletion is done by deleting from the array and updating it.

object.files.remove(at:0)
object.update()

Nested Collection & Reference Collection

NestedCollection and ReferenceCollection are classes that define SubCollection.

When holding File in SubCollection, saving of File will be executed first. When many Files are stored in SubCollection at once, the performance deteriorates.

Nested Collection

  • NestedCollection nests data and saves it under the document.
  • The destination path of File is nested path.

Reference Collection

  • ReferenceCollection saves the documentID under the document.
  • Data is saved separately.
@objcMembersclassUser:Object{dynamicvarname:String?dynamicvarfollowers:ReferenceCollection<User>=[]dynamicvaritems:NestedCollection<Item>=[]}@objcMembersclassItem:Object{dynamicvarthumbnail:File?}letuserA:User=User()
userA.name ="userA"letuserB:User=User()
userB.name ="userB"letitem:Item=Item()
item.thumbnail =File(data: JPEG_DATA, mimeType:.jpeg)
userA.followers.insert(userB)
userA.items.insert(item)
userA.save()
letitem:Item=Item()
userA.items.insert(item)
userA.update(){ error iniflet error = error {
// error handling
return}
// do something
}

DataSource

DataSource is a class for easy handling of data retrieval from Collection.

classDataSourceViewController:UITableViewController{vardataSource:DataSource<User>?overridefunc viewDidLoad(){
super.viewDidLoad()self.dataSource =User.order(by: \User.createdAt).limit(to:30).dataSource().on({[weak self](snapshot, changes)inguardlet tableView:UITableView=self?.tableView else{return}switch changes {case.initial:
tableView.reloadData()case.update(let deletions,let insertions,let modifications):
tableView.beginUpdates()
tableView.insertRows(at: insertions.map{IndexPath(row: $0, section:0)}, with:.automatic)
tableView.deleteRows(at: deletions.map{IndexPath(row: $0, section:0)}, with:.automatic)
tableView.reloadRows(at: modifications.map{IndexPath(row: $0, section:0)}, with:.automatic)
tableView.endUpdates()case.error(let error):print(error)}}).listen()}
// MARK: - Table view data source
overridefunc tableView(_ tableView:UITableView, numberOfRowsInSection section:Int)->Int{returnself.dataSource?.count ??0}overridefunc tableView(_ tableView:UITableView, cellForRowAt indexPath:IndexPath)->UITableViewCell{letcell:DataSourceViewCell= tableView.dequeueReusableCell(withIdentifier:"DataSourceViewCell", for: indexPath)as!DataSourceViewCellconfigure(cell, atIndexPath: indexPath)return cell
}func configure(_ cell:DataSourceViewCell, atIndexPath indexPath:IndexPath){guardlet user:User=self.dataSource?[indexPath.item]else{return}
cell.textLabel?.text = user.name
cell.disposer = user.listen{(user, error)in
cell.textLabel?.text = user?.name
}}func tableView(_ tableView:UITableView, didEndDisplaying cell:DataSourceViewCell, forRowAt indexPath:IndexPath){
cell.disposer?.dispose()}overridefunc tableView(_ tableView:UITableView, canPerformAction action:Selector, forRowAt indexPath:IndexPath, withSender sender:Any?)->Bool{returntrue}overridefunc tableView(_ tableView:UITableView, commit editingStyle:UITableViewCellEditingStyle, forRowAt indexPath:IndexPath){if editingStyle ==.delete {self.dataSource?.removeDocument(at: indexPath.item)}}}

SubCollection DataSource

User.get("USER_ID"){(user, error)inguardlet user:User= user else{return}self.dataSource = user.followers.order(by: \User.createdAt).dataSource().on{(snapshot, changes)in
// something
}.listen()}

Synchronous Client Side Join

@objcMembersclassUser:Object{letgroup:Reference<Group>=Reference()}

Please add on(parse:) to DataSource.

self.dataSource =User.order(by: \User.updatedAt).dataSource().on({[weak self](snapshot, changes)inguardlet tableView:UITableView=self?.tableView else{return}debugPrint("On")switch changes {case.initial:
tableView.reloadData()case.update(let deletions,let insertions,let modifications):
tableView.beginUpdates()
tableView.insertRows(at: insertions.map{IndexPath(row: $0, section:0)}, with:.automatic)
tableView.deleteRows(at: deletions.map{IndexPath(row: $0, section:0)}, with:.automatic)
tableView.reloadRows(at: modifications.map{IndexPath(row: $0, section:0)}, with:.automatic)
tableView.endUpdates()case.error(let error):print(error)}}).on(parse:{(snapshot, user, done)in
user.group.get({(group, error)indone(user)})}).onCompleted({(snapshot, users)indebugPrint("completed")}).listen()

Query

Get documents

User.where(\User.name, isEqualTo:"name").get{(snapshot, error)inprint(snapshot?.documents)}

Get SubCollections

WHERE

letuser:User=User(id:"user_id")
user.items.where(\Item.name, isEqualTo:"item_name").get{(snapshot, error)inprint(snapshot?.documents)}

ORDER

letuser:User=User(id:"user_id")
user.items.order(by: \Item.updatedAt).get{(snapshot, error)inprint(snapshot?.documents)}

Create DataSource from Query

letuser:User=User(id:"user_id")
user.items
.where(\Item.name, isEqualTo:"item_name").dataSource().on({(snapshot, change)in
// do something
}).onCompleted{(snapshot, items)inprint(items)}

Full-text search

Please use ElasticSearch or Algolia when performing full-text search on Firebase. There is a library when implementing with Swift.

https://github.com/miuP/Algent

About

Cloud Firestore model framework for iOS - Google

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages