Add a Document's Document ID to Its Own Firestore Document - Swift 4 - ios

How do I go about adding the document ID of a document I just added to my firestore database, to said document?
I want to do this so that when a user retrieves a "ride" object and chooses to book it, I can know which specific ride they've booked.
The problem that i'm facing is that you can't get the document ID until after it's created, so the only way to add it to said document would be to create a document, read its ID, then edit the document to add in the ID. At scale this would create twice as many server calls as desired.
Is there a standard way to do this? Or a simple solution to know which "ride" the user booked and edit it accordingly in the database?
struct Ride {
var availableSeats: Int
var carType: String
var dateCreated: Timestamp
var ID: String // How do I implement this?
}
func createRide(ride: Ride, completion: #escaping(_ rideID: String?, _ error: Error?) -> Void) {
// Firebase setup
settings.areTimestampsInSnapshotsEnabled = true
db.settings = settings
// Add a new document with a generated ID
var ref: DocumentReference? = nil
ref = db.collection("rides").addDocument(data: [
"availableSeats": ride.availableSeats,
"carType": ride.carType,
"dateCreated": ride.dateCreated,
"ID": ride.ID,
]) { err in
if let err = err {
print("Error adding ride: \(err)")
completion(nil, err)
} else {
print("Ride added with ID: \(ref!.documentID)")
completion(ref?.documentID, nil)
// I'd currently have to use this `ref?.documentID` and edit this document immediately after creating. 2 calls to the database.
}
}
}

While there is a perfectly fine answer, FireStore has the functionality you need built in, and it doesn't require two calls to the database. In fact, it doesn't require any calls to the database.
Here's an example
let testRef = self.db.collection("test_node")
let someData = [
"child_key": "child_value"
]
let aDoc = testRef.document() //this creates a document with a documentID
print(aDoc.documentID) //prints the documentID, no database interaction
//you could add the documentID to an object etc at this point
aDoc.setData(someData) //stores the data at that documentID
See the documentation Add a Document for more info.
In some cases, it can be useful to create a document reference with an
auto-generated ID, then use the reference later. For this use case,
you can call doc():
You may want to consider a slightly different approach. You can obtain the document ID in the closure following the write as well. So let's give you a cool Ride (class)
class RideClass {
var availableSeats: Int
var carType: String
var dateCreated: String
var ID: String
init(seats: Int, car: String, createdDate: String) {
self.availableSeats = seats
self.carType = car
self.dateCreated = createdDate
self.ID = ""
}
func getRideDict() -> [String: Any] {
let dict:[String: Any] = [
"availableSeats": self.availableSeats,
"carType": self.carType,
"dateCreated": self.dateCreated
]
return dict
}
}
and then some code to create a ride, write it out and leverage it's auto-created documentID
var aRide = RideClass(seats: 3, car: "Lincoln", createdDate: "20190122")
var ref: DocumentReference? = nil
ref = db.collection("rides").addDocument(data: aRide.getRideDict() ) { err in
if let err = err {
print("Error adding document: \(err)")
} else {
aRide.ID = ref!.documentID
print(aRide.ID) //now you can work with the ride and know it's ID
}
}

I believe that if you use Swift's inbuilt ID generator, called UUID, provided by the Foundation Framework, this will let you do what you want to do. Please see the below code for my recommended changes. Also by doing it this way, when you first initialise your "Ride" struct, you can generate its ID variable then, instead of doing it inside the function. This is the way I generate unique ID's throughout my applications and it works perfectly! Hope this helps!
struct Ride {
var availableSeats: Int
var carType: String
var dateCreated: Timestamp
var ID: String
}
func createRide(ride: Ride, completion: #escaping(_ rideID: String, _ error: Error?) -> Void) {
// Firebase setup
settings.areTimestampsInSnapshotsEnabled = true
db.settings = settings
// Add a new document with a generated ID
var ref: DocumentReference? = nil
let newDocumentID = UUID().uuidString
ref = db.collection("rides").document(newDocumentID).setData([
"availableSeats": ride.availableSeats,
"carType": ride.carType,
"dateCreated": ride.dateCreated,
"ID": newDocumentID,
], merge: true) { err in
if let err = err {
print("Error adding ride: \(err)")
completion(nil, err)
} else {
print("Ride added with ID: \(newDocumentID)")
completion(newDocumentID, nil)
}
}
}

This is my solution which works like a charm
let opportunityCollection = db.collection("opportunities")
let opportunityDocument = opportunityCollection.document()
let id = opportunityDocument.documentID
let data: [String: Any] = ["id": id,
"name": "Kelvin"]
opportunityDocument.setData(data) { (error) in
if let error = error {
completion(.failure(error))
} else {
completion(.success(()))
}
}

Related

Cannot cast/decode data from server into Swift data model?

I need to cast the below response from my server as [UserResult] but I cannot get it to work??
What am I doing wrong?
func userSearch(keyword: String, completion: #escaping (Result<[UserResult], ResponseError>) -> Void ) {
socket.emit("userSearch", keyword)
socket.on("userFound") { ( data, ack) in
print(data) // prints below NSArray
if !data.isEmpty {
if let response = data as? [UserResult] {
print("USERS \(response)") // WILL NOT WORK?
completion(.success(response))
}
} else {
completion(.failure(.badRequest("No users found")))
}
}
}
Data from server
[<__NSArrayM 0x60000040e5b0>(
{
profileUrl = "www.address1.com";
username = chrissmith;
},
{
profileUrl = "www.address2.com";
username = johnsmith;
},
{
profileUrl = "www.address3.com";
username = alicesmith;
}
)
]
UserResult Model
struct UserResult: Decodable {
let username: String
let profileUrl: String
}
Well you are using Socket.IO library and specifically method
socket.on(clientEvent: .connect) {data, ack in
...
}
defined as
#discardableResult
open func on(clientEvent event: SocketClientEvent, callback: #escaping NormalCallback) -> UUID
using typealias:
public typealias NormalCallback = ([Any], SocketAckEmitter) -> ()
So basically at the and you are being returned data of type [Any] according to documentation.
Since you do not know what is inside your data it is better for you to unwrap objects in your array one by one (instead casting it directly to [UserResult]) and try to find out what Type there are by comparing to some set of known types as some of answers from this question suggest.
I would start with verifying the data structure with example code below , and only move on with casting to various type afterwards:
Lets assume example data1 is your data:
let dict1 = ["profileUrl":"www.address1.com","username":"chrissmith"]
let data1: NSArray = [dict1]
//printed data1:
// (
// {
// profileUrl = "www.address1.com";
// username = chrissmith;
// }
// )
if data1[0] as? [String:String] != nil {
print("We found out that first object is dictionary of [String:String]!")
}
else if data1[0] as? Dictionary<NSObject, AnyObject> != nil {
print("We found out that first object is dictionary of mixed values!")
} else {
print("We found out that first object has different data structure")
}
Hopefully this answer was at least a little bit helpfull, even though not providing direct easy solution for your problem.

what is the best way to reuse API manager class and store data so that it is reusable

I have a PageViewController which creates instances of DataViewController, and this dataViewController needs to do some API called (from manager Class) then use that data to populate its UI. I.E. a page is added, the dataViewController gets its datasource and uses that to tell the API what to get.
The problem is, as I can add multiple pages, and I need for example data to form a graph for each page, having to do the API call every time the view controller loads I feel is unness esary, and that there must be a better way to store the data for the life of the session once it have been retrieved. as each instance of dataViewController uses the same API Managers, I refresh them clearing any data they have, and start the API call again for the page that loaded, this can sometimes get data mixed up, displaying a mixture of information from two pages or displaying the wrong data on the wrong page. Here is an example of one of my API managers which uses the dataObject of the dataViewController to get chart data:
import Foundation
struct Root: Codable {
let prices: [Price]
}
struct Price: Codable {
let date: Date
let price: Double
}
class CGCharts {
var priceData = [Price]()
var graphPrices: [Double] = []
var graphTimes: [String] = []
static let shared = CGCharts()
var defaultCurrency = UserDefaults.standard.string(forKey: "DefaultCurrency")!
var coin = ""
var currency = ""
var days = ""
enum GraphStatus {
case day, week, month
}
var graphSetup = GraphStatus.day
func getData(arr: Bool, completion: #escaping (Bool) -> ()) {
switch graphSetup {
case .day:
days = "1"
case .week:
days = "14"
case .month:
days = "30"
}
let urlJSON = "https://api.coingecko.com/api/v3/coins/\(coin)/market_chart?vs_currency=\(defaultCurrency)&days=\(days)"
guard let url = URL(string: urlJSON) else { return }
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else { return }
do {
let prices = try JSONDecoder().decode(Root.self, from: data).prices
print(prices.first!.date.description(with:.current)) // "Saturday, September 1, 2018 at 6:25:38 PM Brasilia Standard Time\n"
print(prices[0].price)
self.priceData = prices
for element in prices {
self.graphPrices.append(element.price)
}
for element in prices {
self.graphTimes.append(element.date.description(with: nil))
}
completion(arr)
} catch {
print(error)
}
}.resume()
}
func refresh(arr: Bool, completion: #escaping (Bool) -> ()) {
defaultCurrency = UserDefaults.standard.string(forKey: "DefaultCurrency")!
graphPrices = []
graphTimes = []
completion(arr)
}
}
Here as you can see I am getting the data, using codable to parse and in this case the best way I could find to seperate the data was to use the two arrays, I basically want to have thoes 2 arrays stored in memory for that specific dataObject, so that when a page with the same dataObject loads in the pageView it will first check if that data already exists, and use that, instead of getting new information everytime and me having to clear the class. Hope I am making sense, Thank you.
After researching more about NSCache, I learned you can save data to the disk or cache and access it again easily. I found this excellent storage class made by Saoud M. Rizwan here
which makes it super easy to store structs:
var messages = [Message]()
for i in 1...4 {
let newMessage = Message(title: "Message \(i)", body: "...")
messages.append(newMessage)
}
Storage.store(messages, to: .documents, as: "messages.json")
and retrieve them:
let messagesFromDisk = Storage.retrieve("messages.json", from: .documents, as: [Message].self)
Perfect for storing data from an API so that you don't have to make so many API calls, which is especially useful if your requests are limited. In my case, I use the method for checking if storage exists, and if it does I use that to load my data, and if not then I do an api call, store the data using this class, and then run the check again to see that the data is there and use that, It may not be the most efficient but it works.
my example:
var prices: [Double] = []
var days: [String] = []
var priceData = [Price]()
var graphDataExists = false
func checkGraphStorage() {
if Storage.fileExists("\(dataObject)GraphPrices", in:
Storage.Directory.caches) {
print("UsingSavedData")
self.priceData = Storage.retrieve("\(dataObject)GraphPrices", from:
Storage.Directory.caches, as: [Price].self)
if self.prices.isEmpty {
for element in self.priceData {
self.prices.append(element.price)
}
for element in self.priceData {
self.days.append(element.date.description(with: nil))
graphDataExists = true
}
} else {
}
DispatchQueue.main.async {
self.chartView.animate(xAxisDuration: 4.0)
self.lineChartUpdate(dataPoints: self.days, values: self.prices)
}
} else {
self.prepareGraph(arr: true) { (success) in
}
}
}

Initialize object with Firestore data within a model

I'm am looking for a nice an clean way to assign and get the user/author from my post object.
I could possible loop throw each Post in the posts array and get the user data from Firestore and assign it to the post property from within the viewcontroller, but what I'm looking for is like a computed property or lazy property that gets the data from Firestore and adds it to the object automatically when Post is initialized.
I don't no if it is possible or if it is the right way to do it, been struggling trying different methods with out any success.
This is my current post model - Post.swift
struct Post {
var author: User?
let authorUID: String
let content: String
init(authorUID: String, content: String) {
self.authorUID = authorUID
self.content = content
}
var dictionary: [String:Any] {
return [
"authorUID": authorUID,
"content": content
]
}
}
In my viewController I have a array of post witch get filled by data from the Firestore database - PostViewController.swift
func loadAllPosts() {
database.collection("posts").getDocuments { (querySnapshot, error) in
if let error = error {
print("Error when loading uhuus: \(error.localizedDescription)")
} else {
self.posts = querySnapshot!.documents.flatMap({Post(dictionary: $0.data())})
DispatchQueue.main.async {
self.tableview.reloadData()
}
}
}
}
This is what I was imagining my Post model would look like - Post.swift
(This code does not work and returns a "Unexpected non-void return value in void function" error)
struct Post {
private var database = Firestore.firestore()
var author: User?
let authorUID: String
let content: String
init(authorUID: String, content: String) {
self.authorUID = authorUID
self.content = content
}
var dictionary: [String:Any] {
return [
"authorUID": authorUID,
"content": content
]
}
private var setUser: User {
database.collection("users").document(authorUID).getDocument { (document, error) in
if let user = document.flatMap({ User(dictionary: $0.data()) }) {
return user
} else {
print("Document does not exist")
}
}
}
}
If you don't have a solutions but nows this is bad practice, Then I would like to know why and what would be the best way.
You seem to be on the right track with your second code snippet. You might consider triggering the setUser function when you init a Post as that is the point where you will have a valid authorId.
The reason you are getting that error is because you are trying to return a User in your setUser function, when what you really need to do is just update your user variable on that particular Post.
I'd suggest updating like so:
struct Post {
private var database = Firestore.firestore()
var author: User?
let authorUID: String
let content: String
init(authorUID: String, content: String) {
self.authorUID = authorUID
self.content = content
//Immediately fetch the User object here
self.setUser()
}
var dictionary: [String:Any] {
return [
"authorUID": authorUID,
"content": content
]
}
private func setUser() {
database.collection("users").document(authorUID).getDocument { (document, error) in
if let user = document.flatMap({ User(dictionary: $0.data()) }) {
//Set the user on this Post
self.author = user
} else {
print("Document does not exist")
}
}
}
}
On a somewhat unrelated note, you might want to avoid having a database reference inside each Post object. It will still work if you continue the way you have it here, but it's not the Post's job to keep track of this. It'd make more sense to have it in a separate class that is built to deal with database functions.

Error when QuerySnapshot runs in Firestore - Throws error Skipping epollex becuase skipping EPOLLEX GRPC_LINUX_EPOLL is not defined

I have created a function to pull a random document from my firestore collection, which then returns a FIRQuery. Once i got that, I created a model file to parse any FIRQuery data from my firestore document. However, once i run it and attempt to print it to the log, I get the response of "Skipping epollsig becuase GRPC_LINUX_EPOLL is not defined." 4 times, which makes sense as i am trying to print 4 fields of my document. My code for the parsing is below - keep in mind any CAPS values are constants that are just defined as equal to exactly how they read in the firestore document field
class randomModel {
private(set) var quote: String!
private(set) var randomInt: Int!
private(set) var approved: Bool!
private(set) var source: String!
init(approved: Bool, quote: String, randomInt: Int, source: String)
{
self.approved = approved
self.quote = quote
self.randomInt = randomInt
self.source = source
}
class func parseData(snapshot: QuerySnapshot?) -> [randomModel] {
var quoteStructure = [randomModel]()
guard let snap = snapshot else {return quoteStructure}
for document in (snap.documents) {
let data = document.data()
let quote = data[DATABASE_QUOTE_VALUE_FIELD] as? String
let randomInt = data[DATABASE_RANDOM_INTEGER_VALUE_FIELD] as? Int
let approved = data[DATABASE_APPROVED_VALUE_FIELD] as? Bool
let source = data[DATABASE_SOURCE_VALUE_FIELD] as? String
let parsedData = randomModel(approved: approved!, quote: quote!, randomInt: randomInt!, source: source!)
quoteStructure.append(parsedData)
}
return quoteStructure
}
Then my code to actually print the data and pull a random document is here:
#IBAction func generatePressed(_ sender: Any) {
let quoteRef = Firestore.firestore().collection("Quotes")
let queryRef = quoteRef.whereField(DATABASE_RANDOM_INTEGER_VALUE_FIELD, isGreaterThan: 1)
.order(by: "randomInt")
.limit(to: 1)
.addSnapshotListener { (snapshot, error) in
if let err = error {
debugPrint("Error Fetching Document \(err)")
} else {
print(randomModel.parseData(snapshot: snapshot))
}
}
}
So can someone guide me as to what I am doing wrong to throw this error. My Document structure looks like this in firestore.. sorry, I don't have any rep to be able to embed into a post yet. Thanks for the help in advance!!
This is a bug that was fixed in gRPC 1.8.4. Run pod update.

How do I check if a Firestore document exists before I update it?

I'm newish to Swift and new to Firestore and am running into an issue that I can't solve. I have the code below which is supposed to check for a document at the UserReference location in Firestore. If it doesn't exist, it should create a new document that contains the pertinent user information that I have previously grabbed from facebook.
This what UserReference looks like self.UserReference = self.db.collection("users").document(UserID as! String) where the UserID is from the Facebook graph request. Next, it'll run a transaction to pull the document, update the user doc with the latest facebook info (assuming this is not a new user), and then update the doc to Firebase Firestore.
let db = Firestore.firestore()
var fbUserUpdate = User(
firstName: String(),
lastName: String(),
email: String(),
<extra stuff>)
func updateFacebookInfoToFirestore(completion: #escaping (_: User?, _: Error?) -> Void) {
// Check if user exists, if not create new user
UserReference!.getDocument { (document, error) in
if document != nil {
// continue
} else {
self.UserReference.setData(self.fbUserUpdate.dictionary)
}
}
// Now pull data and update based on most recent facebook info.
db.runTransaction({ (transaction, errorPointer) -> Any? in
// Read data from Firestore inside the transaction, so we don't accidentally
// update using staled client data. Error if we're unable to read here.
let UserSnapshot: DocumentSnapshot
do {
try UserSnapshot = transaction.getDocument(self.UserReference!)
} catch let error as NSError {
errorPointer?.pointee = error
return nil
}
// This will overwrite the fbUserUpdate Struct with the updated information.
guard var dbUser = User(dictionary: UserSnapshot.data()) else {
let error = NSError(domain: "Domain", code: 0, userInfo: [
NSLocalizedDescriptionKey: "Unable to write to userdata at Firestore path: \(self.UserReference!.path)"
])
errorPointer?.pointee = error
return nil
}
// Update from Facebook
dbUser.firstName = self.fbUserUpdate.firstName
dbUser.lastName = self.fbUserUpdate.lastName
dbUser.email = self.fbUserUpdate.email
// Load new data to firestore
transaction.setData(dbUser.dictionary, forDocument: self.UserReference!, options: SetOptions.merge())
return nil
}) { (object, error) in
if let error = error {
print(error)
} else {
// nothing yet
}
}
}
However, when I run this in my app, when I get to the UserReference!.getDocument closure, it skips over the closure and then the transaction doesn't work as intended because the try UserSnapshot = transaction.getDocument(self.UserReference!) returns a null since no document exists.
I believe the issue is in the .getDocument closure, but I don't know where I'm going wrong. I've tried to emulate the FriendlyEats firestore example code as best I can but I'm stuck and am in need of an extra set of eyes.

Resources