In a tableView I have a list of jobs. These jobs can be accessed by multiple users, therefore I need to use FIRTransaction. Based on the result of the first write to FirebaseDatabase, I need to write/not write to another path in Firebase.
The schema is as follows:
//Claim job by - Cleaner
//cleaner tries to claim booking at Users/UID/cus/bookingNumber
//if FIRMutableData contains a key ‘claimed’ or key:value “claimed”:true
//update at Users/UID/cus/bookingNumber with key:value
//based on response received write or not to another path
//write to Cleaners/UID/bookingNumber
//key:value
If the internet connection drops before client app receives response from firebase server, write to Cleaners/UID/bookingNumber will not be made.
How can I solve this problem?
#IBAction func claimJob(_ sender: Any) {
dbRef.runTransactionBlock({ (_ currentData:FIRMutableData) -> FIRTransactionResult in
//if valueRetrieved is nil abort
guard let val = currentData.value as? [String : AnyObject] else {
return FIRTransactionResult.abort()
}
self.valueRetrieved = val
guard let uid = FIRAuth.auth()?.currentUser?.uid else {
print("abort no uid line 80")
return FIRTransactionResult.abort()
}
self.uid = uid
for key in self.valueRetrieved.keys {
//unwrap value of 'claimed' key
guard let keyValue = self.valueRetrieved["Claimed"] as? String else {
print("abort line 88")
return FIRTransactionResult.abort()
}
//check if key value is true
if keyValue == "true"{
//booking already assigned show alert,stop transaction
self.alertText = "Booking already taken, please refresh table!"
self.alertActionTitle = "OK"
self.segueIdentifier = "unwindfromClaimDetailToClaim"
self.showAlert()
return FIRTransactionResult.abort()
} else {
//write the new values to firebase
let newData = self.createDictionary()
currentData.value = newData
return FIRTransactionResult.success(withValue: currentData)
}//end of else
}//end of for key in self
return FIRTransactionResult.abort()
}) {(error, committed,snapshot) in
if let error = error {
//display an alert with the error, ask user to try again
self.alertText = "Booking could not be claimed, please try again."
self.alertActionTitle = "OK"
self.segueIdentifier = "unwindfromClaimDetailToClaim"
self.showAlert()
//what if internet connection drops here or client quits app ????????????
} else if committed == true {
//write to Cleaners/UID/bookingNumber
//what if internet connection drops here or client quits app??????
self.cleanersRef.setValue(snapshot?.value)
self.alertText = "Booking claimed.Please check your calendar"
self.alertActionTitle = "OK"
self.segueIdentifier = "unwindfromClaimDetailToClaim"
self.showAlert()
}
}
}//end of claimJob button
Related
I have a collection on Firestore and I listen for changes like this:
func createMatchesListener(){
let db = Firestore.firestore()
guard let currentUid = Auth.auth().currentUser?.uid else { return }
matchesListener = db.collection("Matches").document(currentUid).collection("Matches").addSnapshotListener({ snapshot, error in
if let error = error{
print(error.localizedDescription)
return
}
snapshot?.documentChanges.forEach({ change in
if change.type == .added{
// do things
}
})
})
}
I only want to listen for documents that are actually added to that collection.
In fact, the problem is that whenever I invoke this function I receive all the documents of the collection as added documents and then I also receive documents added later.
How can I listen just for actually added later documents, ignoring the ones already present in the collection? Searching online I didn't find any solution to this issue.
EDIT:
This is the way I tried to solve the problem:
func createMatchesListener(){
guard let currentUid = Auth.auth().currentUser?.uid else { return }
getUidsAlreadyMade { uidsAlreadyMade in
matchesListener = db.collection("Matches").document(currentUid).collection("Matches").addSnapshotListener({ snapshot, error in
if let error = error{
print(error.localizedDescription)
return
}
snapshot?.documentChanges.forEach({ change in
if change.type == .added{
let data = change.document.data()
let userId = data["uid"] as? String ?? ""
if uidsAlreadyMade.contains(userId) == false{
//means the uid is newly created in the collection, do stuff accordingly
arrayOfUidsAlreadyMade.append(currentUid)
}
}
if change.type == .removed{
// if the document has been removed, remove also the id from the array of uids
let data = change.document.data()
let currentUid = data["uid"] as? String ?? ""
arrayOfUidsAlreadyMade.removeAll { $0 == currentUid }
}
})
})
}
}
func getUidsAlreadyMade(completion: #escaping ([String]) -> Void){
guard let currentUid = Auth.auth().currentUser?.uid else { return }
db.collection("Matches").document(currentUid).collection("Matches").getDocuments { snapshot, error in
if let error = error{
print(error.localizedDescription)
return
}
arrayOfUidsAlreadyMade.removeAll()
snapshot?.documents.forEach({ doc in
let dict = doc.data()
let userId = dict["uid"] as? String ?? ""
arrayOfUidsAlreadyMade.append(userId)
})
completion(arrayOfUidsAlreadyMade)
}
}
A simple solution is to include a timestamp in your Firestore documents.
Suppose your documents store Tasks, for example
documentId
task: "get dinner"
timestamp: 20211123
and suppose your app doesn't care about past tasks, only new ones.
When the tasks are read, update the timestamp as to when that occurred.
Then each time after that you want to read only 'new data' specify that in your listener, keeping track of when the last read timestamp was:
db.collection("task").whereField("timestamp", isGreaterThan: lastReadTimeStamp).addSnapshotListener...
The above will only read in tasks that occured after the prior timestamp and add a Listener (reading in all of the new tasks so you can populate the UI).
You can store an array with the ID of the documents that you already have stored in the device. That way, all that you need to do before doing things is checking that document's id is not in your array
There's no way of preventing Firestore from returning the initial snapshot of documents when a document listener is added, so just use a boolean to keep track of the initial snapshot and ignore it.
var listenerDidInit = false
func createMatchesListener(){
let db = Firestore.firestore()
guard let currentUid = Auth.auth().currentUser?.uid else { return }
matchesListener = db.collection("Matches").document(currentUid).collection("Matches").addSnapshotListener({ snapshot, error in
if let error = error{
print(error.localizedDescription)
return
}
if listenerDidInit {
snapshot?.documentChanges.forEach({ change in
if change.type == .added{
// do things
}
})
} else {
listenerDidInit = true
}
})
}
private var listener: ListenerRegistration?
self.listener = db.collection("Matches") // matchesListener
listener!.remove()
Error Domain=MCOErrorDomain Code=5 "Unable to authenticate with the current session's credentials." UserInfo={NSLocalizedDescription=Unable to authenticate with the current session's credentials.}
I put this code in my project.
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
prepareImapSession()
}
var imapsession:MCOIMAPSession = MCOIMAPSession()
var error : Error? = nil
func prepareImapSession()
{
// CONFIGURE THAT DEPENDING OF YOUR NEEDS
imapsession.hostname = "imap.gmail.com" // String
imapsession.username = my email // String
imapsession.password = password // String
imapsession.port = 993 // UInt32 number
imapsession.authType = MCOAuthType.saslLogin
imapsession.connectionType = MCOConnectionType.TLS
DispatchQueue.main.asyncAfter(deadline: .now() + 8) { [self] in
self.useImapWithUIDS()
}
imapsession.connectOperation()
}
func useImapWithUIDS() {
// There is more than one option here, explore depending of your needs
// let kind = MCOIMAPMessagesRequestKind()
// let headers = kind.union(MCOIMAPMessagesRequestKind.headers)
// let request = headers.union(MCOIMAPMessagesRequestKind.flags)
let requestKind: MCOIMAPMessagesRequestKind = [.headers, .flags]
let folder : String = "INBOX"
// HERE ALSO EXPLORE DEPENDING OF YOUR NEEDS, RANGE IT IS THE RANGE OF THE UIDS THAT YOU WANT TO FETCH, I SUGGEST TO YOU TO CHANGE THE // NUMBER ONE IF YOU HAVE A LOWER BOUND TO FETCH EMAIL
let uids : MCOIndexSet = MCOIndexSet(range: MCORangeMake(1, UINT64_MAX))
let fetchOperation = imapsession.fetchMessagesOperation(withFolder: folder, requestKind: requestKind, uids: uids)
fetchOperation?.start
{ [self] (err, msg, vanished) -> Void in
if (err != nil)
{
self.error = err
NSLog((err?.localizedDescription)!)
}
else
{
guard let msgs = msg as? [MCOIMAPMessage]
else
{
print("ERROR GETTING THE MAILS")
return
}
for i in 0..<msgs.count
{
// THE SUBJECT
let subject = msgs[i].header.subject
// THE uid for this email. The uid is unique for one email
let uid = msgs[i].uid
self.useImapFetchContent(uidToFetch: uid)
// The sequenceNumber like the nomber say it is the sequence for the emails in the INBOX from the first one // (sequenceNumber = 1) to the last one , it not represent always the same email. Because if you delete one email then //next one will get the sequence number of that email that was deleted
let sequenceNumber = msgs[i].sequenceNumber
}
}
}
}
// MARK: - EXTRACT THE CONTENT OF ONE EMAIL, IN THIS FUNCTION YOU NEED THE uid, THE UNIQUE NUMBER FOR ONE EMAIL
func useImapFetchContent(uidToFetch uid: UInt32) {
let operation: MCOIMAPFetchContentOperation = imapsession.fetchMessageOperation(withFolder: "INBOX", uid: uid)
operation.start { (Error, data) in
if (Error != nil)
{
NSLog("ERROR")
return
}
let messageParser: MCOMessageParser = MCOMessageParser(data: data)
// IF YOU HAVE ATTACHMENTS USE THIS
let attachments = messageParser.attachments() as? [MCOAttachment]
// THEN YOU NEED THIS PROPERTIE, IN THIS EXAMPLE I TAKE THI FIRST, USE WHAT EVER YOU WANT
let attachData = attachments?.first?.data
// FOR THE MESSAGEPARSER YOU CAN EPLORE MORE THAN ONE OPTION TO OBTAIN THE TEXT
let msgPlainBody = messageParser.plainTextBodyRendering()
}
}
}
I using the mailcore2 framework. I got error description Unable to authenticate with the current session's credentials.
It can be related with 2nd factor authentication on your account. It was described on Apple forum.
Hi I am new in iOS development and I am having hard time to understand the following issue. Basically I am trying to get user's name by passing current user's id to Cloud Firestore. However I am having hard time to understand a bug in the code. I can successfully pass the name of user to name variable, while the function returns default value of name which is "" (empty string). It seems that the block of code inside
if let data = snapshot?.data() {
guard let userName = data["name"] as? String else { return }
name = userName
print("after guard") // this line
}
happens later than
print("name") // this line
return name
Full code:
private func returnCurrentUserName() -> String {
// User is signed in.
var name = ""
if let user = Auth.auth().currentUser {
let db = Firestore.firestore()
db.collection("users").document(user.uid).getDocument { (snapshot, error) in
if error == nil {
if let data = snapshot?.data() {
guard let userName = data["name"] as? String else { return }
name = userName
print("after guard") // this line
}
}
}
print("name") // this line
return name
}else {
return ""
}
}
(Note: the query from Cloud Firestore is successful and I can get users name on the console but "name" is printed after "after guard".)
In addition to the other answer:
If you would like to execute code after your operation is done, you could use a completion block (that's just a closure which gets called upon completion):
private func returnCurrentUserName(completion: #escaping () -> ()) -> String {
// User is signed in.
var name = ""
if let user = Auth.auth().currentUser {
let db = Firestore.firestore()
db.collection("users").document(user.uid).getDocument { (snapshot, error) in
if error == nil {
if let data = snapshot?.data() {
guard let userName = data["name"] as? String else { return }
name = userName
completion()//Here you call the closure
print("after guard") // this line
}
}
}
print("name") // this line
return name
}else {
return ""
}
}
How you would call returnCurrentUserName:
returnCurrentUserName {
print("runs after the operation is done")
}
Simplified example:
func returnCurrentUserName(completion: #escaping () -> ()) -> String {
DispatchQueue.main.asyncAfter(deadline: .now() + 4) {
completion() //runs after 4 seconds
}
return "xyz"
}
let test = returnCurrentUserName {
print("runs after the operation is done")
}
print(test)
The reason is your getDocument is an asynchronous operation. It takes a callback, and that callback will be invoked when the operation is done. Because of the asynchronous operation, the program will continue process the next line without waiting for the async operation to be completed. That's why you see your print("name") getting executed before the print("after guard")
I am wanting to update the child values after editing inside the textfields.
At the moment I have this action:
#IBAction func updateAction(_ sender: Any) {
guard let itemNameText = itemName.text, let itemDateText = itemDate.text else { return }
guard itemNameText.characters.count > 0, itemDateText.characters.count > 0 else {
print("Complete all fields")
return
}
let uid = FIRAuth.auth()?.currentUser?.uid
let key = item.ref!.key
let itemList = Item(itemName: itemNameText, itemDate: itemDateText, uid: uid!)
let editItemRef = databaseRef.child("/usersList/\(key)")
editItemRef.updateChildValues(itemList.toAnyObject())
print("edited")
}
I was following this tutorial but he seems to use the username, and as I only have the email or uid (userID) as authentication I thought I'd use the uid.
This is my toAnyObject function inside my class:
func toAnyObject() -> [String: AnyObject] {
return ["itemName": itemName as AnyObject, "itemDate": itemDate as AnyObject, "userID": userID as AnyObject]
}
When I run the breakpoint it does show the edited value of the item however the update doesn't appear to be performing.
Just to be extra safe, try dropping the leading slash from your path:
databaseRef.child("usersList/\(key)")
…and try printing the Error returned by Firebase, if any:
editItemRef.updateChildValues(itemList.toAnyObject()) {
(error, _) in
if let error = error {
print("ERROR: \(error)")
} else {
print("SUCCESS")
}
Edit. We found out he was using the wrong database path. The right one is:
databaseRef.child("users").child(uid!).child("usersList/\(key)")
As I understand it, the CKModifyRecordsOperation(recordsToSave:, recordsToDelete:) method should make it possible to modify multiple records and delete multiple records all at the same time.
In my code, recordsToSave is an array with 2 CKRecords. I have no records to delete, so I set recordsToDelete to nil. Perplexingly enough, it appears that recordsToSave[0] gets saved to the cloud properly while recordsToSave[1] does not.
To give some more context before I paste my code:
In my app, there's a "Join" button associated with every post on a feed. When the user taps the "Join" button, 2 cloud transactions occur: 1) the post's reference gets added to joinedList of type [CKReference], and 2) the post's record should increment its NUM_PEOPLE property. Based on the CloudKit dashboard, cloud transaction #1 is occurring, but not #2.
Here is my code, with irrelevant parts omitted:
#IBAction func joinOrLeaveIsClicked(_ sender: Any) {
self.container.fetchUserRecordID() { userRecordID, outerError in
if outerError == nil {
self.db.fetch(withRecordID: userRecordID!) { userRecord, innerError in
if innerError == nil {
var joinedList: [CKReference]
if userRecord!.object(forKey: JOINED_LIST) == nil {
joinedList = [CKReference]() // init an empty list
}
else {
joinedList = userRecord!.object(forKey: JOINED_LIST) as! [CKReference]
}
let ref = CKReference(recordID: self.post.recordID, action: .none)
// ... omitted some of the if-else if-else ladder
// add to list if you haven't joined already
else if !joinedList.contains(ref) {
// modifying user record
joinedList.append(ref) // add to list
userRecord?[JOINED_LIST] = joinedList as CKRecordValue // associate list with user record
// modifying post
let oldCount = self.post.object(forKey: NUM_PEOPLE) as! Int
self.post[NUM_PEOPLE] = (oldCount + 1) as CKRecordValue
let operation = CKModifyRecordsOperation(recordsToSave: [userRecord!, self.post], recordIDsToDelete: nil)
self.db.add(operation)
}
// omitted more of the if-else if-else ladder
else {
if let error = innerError as? CKError {
print(error)
}
}
}
}
else {
if let error = outerError as? CKError {
print(error)
}
}
}
}
EDIT
Here's the code I added per the request of the first commenter
operation.modifyRecordsCompletionBlock = { savedRecords, deletedRecordsIDs, error in
if error == nil {
DispatchQueue.main.async(execute: {
self.num.text = String(oldCount + 1) // UI update
})
}
else {
print(error!)
}
}
ANOTHER EDIT
let operation = CKModifyRecordsOperation(recordsToSave: [userRecord!, self.post], recordIDsToDelete: nil)
operation.perRecordCompletionBlock = { record, error in
if error != nil {
let castedError = error as! NSError
print(castedError)
}
}
operation.modifyRecordsCompletionBlock = { savedRecords, deletedRecordsIDs, error in
if error == nil {
DispatchQueue.main.async(execute: {
self.num.text = String(oldCount + 1) // UI update
})
}
else {
print(error!)
}
}
self.db.add(operation)