I am using Firebase SDK (2.4.2) for iOS. The following code is part of the didSelectPost() function used in SLComposeServiceViewController object. It's very important to indicate that this behavior is not the same when used in a regular UIViewController (weird I know).
Assuming the following basic code:
override func presentationAnimationDidFinish() {
// Retrieving content and identifying type here
}
override func didSelectPost() {
self.myRootRef.runTransactionBlock({
(currentData:FMutableData!) in
currentData.value = "test"
// Finalizing the transaction
return FTransactionResult.successWithValue(currentData)
}
)
self.extensionContext!.completeRequestReturningItems([], completionHandler: nil)
}
When the transaction is complete node_value disappears completely from Firebase. The value is not set and the node is deleted. This is a very weird and unexpected behavior!
On the other hand, the following code works as expected.
override func didSelectPost() {
self.myRootRef.setValue("test", withCompletionBlock: {
(error:NSError?, ref:Firebase!) in
if (error != nil) {
print("Data could not be saved.")
} else {
print("Data saved successfully!")
}
})
self.extensionContext!.completeRequestReturningItems([], completionHandler: nil)
}
This problem did not exist a couple of days ago, last I ran the code above. Any insights as to what the problem is?
If you set nil to a path in a Firebase database, it will do the same as a .removeValue().
When running a transaction you should check for nil. Since transactions can be called with nil if no default value was written.
myRootRef.runTransactionBlock({
(currentData:FMutableData!) in
var value = currentData.value as? Int
if value == nil {
value = 0
}
currentData.value = value! + 1
return FTransactionResult.successWithValue(currentData)
})
Read the Firebase Docs for more information.
Related
I am using firestore along with iglistkit to display data on collection view. I am trying to understand why my snapshot listener gets called twice with the same object.
issue summary:
On Viewdidload I call the fetchUserFriends() method and receive the documents that I am expecting from the querySnapshot but for some unknown reason, method body gets called twice, without any changes being made to the data.
The problematic code is in below:
func fetchUserFriends() {
guard let currentUserId = currentUser?.uid else { return }
db.collection("friends").whereField(FriendState.isRelationshipActive, isEqualTo: true).whereField("members", arrayContains: currentUserId).order(by: "createdAt", descending: true).addSnapshotListener { [weak self] (querySnapshot, error) in
if(error != nil) {
print("error \(String(describing: error?.localizedDescription))")
}
guard let querySnapshot = querySnapshot else { return }
for document in querySnapshot.documents {
let friendRelation = UserRelation.init(document: document)
if(self?.friendsRelations != nil) {
self?.friendsRelations?.append(friendRelation)
} else {
self?.friendsRelations = [friendRelation]
}
}
self?.adapter.reloadData(completion: nil)
}
}
Based on my debugging, what happens is that:
fetchUserFriends() gets called
goes through guard let querySnapshot = querySnapshot else { return } and adds the data to friendRelations array
self?.adapter.reloadData(completion: nil)
then the line below runs
and goes back to the 2nd step again with the same object which in this case fails due to iglistkit duplicate identifier.
Thanks for the help.
I'm running in the same issue here. Strangely enough I have different listeners, one for each document. One triggers once. The other one twice. I've found a way to stop it from triggering twice.
if !snapshot.metadata.hasPendingWrites { // filters the local snapshot
let myobj = Myobj(document: snapshot)
completion(myobj)
}
The explanation is that it is tracking the local and the server-side changes and this will filter to get the server-side confirmation only.
Now, the reason why this is triggering the local snapshot for ONLY ONE of the equally setup listeners remains a mystery to me. Hope it can help someone.
Okay, so I'm trying to build an iOS app that relies on Firebase (To work with its android version)
I started with creating a repository for each actor in my app and a general repository to manage them all
Each repository manages the observers of this actor. An example:
Inside the PagesRepository, this is a function that retrieves all the pages from Firebase and returns it inside a completionHandler:
//MARK: Gets the whole pages list
func getPagesList(completionHandler: #escaping (_ pages: [Page]?, _ error: NSError?) -> Void) {
func displayError(error: String) {
print(error)
completionHandler(nil, self.getErrorFromString(error))
}
pagesRef.observe(DataEventType.value) { pagesSnapshot in
guard pagesSnapshot.exists() else {
displayError(error: "Pages snapshot doesn't exist")
return
}
var pagesList = [Page]()
for pageSnapshot in pagesSnapshot.children {
pagesList.append(Page(snapshot: pageSnapshot as! DataSnapshot))
}
completionHandler(pagesList, nil)
}
}
And then I call it from the ViewController like this:
repository.getPagesList { (pages, error) in
guard error == nil else {
return
}
//Do processing
}
I know this may be a lot to take in, but my problem is that every time I call the function, it creates a new observer but doesn't cancel the old one... So, the completionHandler is called multiple times with different values
How should I manage this problem?
(Sorry for being complicated and a little unclear, I'm just really lost)
It seems like you only want to observe the value once so I would use this instead:
func getPagesList(completionHandler: #escaping (_ pages: [Page]?, _ error: NSError?) -> Void) {
func displayError(error: String) {
print(error)
completionHandler(nil, self.getErrorFromString(error))
}
pagesRef.observeSingleEvent(of: .value, with: { (pagesSnapshot) in
var pagesList = [Page]()
for pageSnapshot in pagesSnapshot.children {
pagesList.append(Page(snapshot: pageSnapshot as! DataSnapshot))
}
completionHandler(pagesList, nil)
}) { (error) in
// Display error
}
}
So in my app I just made it so that the user can choose to delete their account, and that works out beautifully. However after the account is deleted the app crashes. I think this is because it is trying to search for a user but it's not there.
Here is my code:
let loginController = LoginController()
func deleteAccount() {
let user = Auth.auth().currentUser
let userId = Auth.auth().currentUser?.uid
let databaseUser = Database.database().reference().child("users").child(userId!)
user?.delete { error in
if let error = error {
print(error)
} else {
self.present(self.loginController, animated: true, completion: nil)
}
}
databaseUser.removeValue(completionBlock: { (error, ref) in
if error != nil {
print(error)
} else {
self.present(self.loginController, animated: true, completion: nil)
} //Without doing this the user's account only gets deleted in the Authentication, not the whole database. I think this is the problem here?
})
}
Thank you so much in advance!
First you need to delete the users database as to do that you would need the let userId = Auth.auth().currentUser?.uid which is only active if the user itself is on your backend, Then you go on to delete the auth.
func deleteAccount() {
let user = Auth.auth().currentUser
let userId = Auth.auth().currentUser!.uid
let databaseUser = Database.database().reference().child("users").child(userId)
databaseUser.removeValue(completionBlock: { (error, ref) in
if error != nil {
print(error)
} else {
user?.delete { error in
if let error = error {
print(error)
} else {
self.present(self.loginController, animated: true, completion: nil)
}
}
})
}
If this still doesn't work track down the lifecycle of the user using debugging tools....
You are implicitly unwrapping an optional in this line with !:
let databaseUser = Database.database().reference().child("users").child(userId!)
You should check whether it's nil in the first place with a guard statement:
guard let userId = Auth.auth().currentUser?.uid else {
return
}
let databaseUser = Database.database().reference().child("users").child(userId)
[...]
Moreover, your code logic is likely to be wrong, as you are getting nil for the userId before you can work with it.
The code order is not ideal because deleting the user also logs them out. So the code may be trying to access the users node after the user was logged out.
Also remember that Firebase is asynchronous and the only way to know a function has completed is when the code inside the closure executes i.e. in this case the databaseUser.removeValue may be firing before the delete user or sometimes it may not.
Code is faster than the internet so it's best to leverage the closures so you know when it's safe to proceed.
Try this sequence; noting that we don't try to delete the Firebase user until we know for sure the data in the users node was deleted. There could use more error checking but you get the idea.
let userRef = self.ref.child("users").child(uid)
userRef.setValue(nil, withCompletionBlock: { snapshot in
Auth.auth().currentUser?.delete(completion: { err in
if err != nil {
print(err?.localizedDescription)
}
})
})
This question already has answers here:
What does "Fatal error: Unexpectedly found nil while unwrapping an Optional value" mean?
(16 answers)
Closed 6 years ago.
I am trying to download image from Firebase storage.
func downloadThumbnail(thumbnail: String) -> URL {
var thumb: URL!
let _ = DataService.dataService.TAG_PHOTO_REF.child("\(thumbnail)").downloadURL { (thumbnailUrl, error) in
if error != nil {
print(error?.localizedDescription as Any)
} else {
thumb = thumbnailUrl
}
}
return thumb
}
cell.photo.kf.setImage(with: downloadThumbnail(thumbnail: selectedTag.thumbnail))
When I run this code I got
fatal error: unexpectedly found nil while unwrapping an Optional value
with return thumb line.
But if I run only print(thumbnailUrl) instead of return, it prints correct thumbnail url. Can anyone know why I got this error?
Thanks.
You can not guarantee that thumb will never be nil. Because of this, you should not be using !. Because you have no control over it and have not set it manually, you need to make it an optional.
var thumb: URL?
Secondly, you have an internet call. You are returning thumb before you get a response from that call, because of that, thumb is nil, but you told us with the ! that that is impossible, so you crash.
If you put in breakpoints, you should notice that you will hit return thumb on your method before you hit the if error != nil line. You can't use a return for this, because the method will always return before it gets a response from firebase, so your URL will always be nil. I would instead send a URL in a completion.
I haven't checked the firebase code, but if all is right with it, this is the order you want.
So:
func downloadThumbnail(thumbnail: String,withCompletion comp: #escaping (URL?, Error?) -> ()) {
let _ = DataService.dataService.TAG_PHOTO_REF.child("\(thumbnail)").downloadURL { (thumbnailUrl, error) in
if error != nil {
print(error?.localizedDescription as Any)
comp(nil, error)
} else {
comp(thumbnailUrl, nil)
}
}
}
So when you call it somewhere else:
func getMyImage(cell: UITableViewCell) {
downloadThumbnail(thumbnail: selectedTag.thumbnail) { (thumbnailUrl, error) in
if error != nil {
//show some sort of alert for the user here? or do something to handle the error?
} else {
//the url is an optional URL, so check to make sure it isn't nil
if let url = thumbnailUrl {
cell.photo.kf.setImage(with: url)
} else {
//you didn't get an error from your firebase response
//but the thumbnail url it gave you is broken for some reason
//so again, do something about your error here
}
}
}
If this doesn't match the design pattern of your app, let me know. I assumed you were using a tableview and that these methods might be in different classes.
My share extension has the following code as part of the didSelectPost() segment:
override func didSelectPost() {
if self.sharedURL != nil {
// Send data to Firebase
self.myRootRef.runTransactionBlock({
(currentData:FMutableData!) in
var value = currentData.value as? String
// Getting the current value
// and checking whether it's null
if value == nil {
value = ""
}
// Setting the new value to the clipboard
// content
currentData.value = self.sharedURL?.absoluteString
// Finalizing the transaction
return FTransactionResult.successWithValue(currentData)
}, andCompletionBlock: {
// Completion Check
(error:NSError!, success:Bool, data:FDataSnapshot!) in
print("DEBUG- We're done:\(success) and \(error)")
}
)
}
// This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments.
// Inform the host that we're done, so it un-blocks its UI. Note: Alternatively you could call super's -didSelectPost, which will similarly complete the extension context.
self.extensionContext!.completeRequestReturningItems([], completionHandler: nil)
}
I'm getting the following error at runtime:
host connection <NSXPCConnection: 0x7fb84af2e8c0> connection from pid 16743 invalidated
I believe this error is due to the andCompletionBlock and related to the following issue: Debug info when run today extension
How can I cleanly and successfully deal with the completion status of the above transaction?
Like the answer you linked to stated, the NSXPCConnection error doesn't matter here.
The issue is that .runTransactionBlock() is asynchronous and .completeRequestReturningItems() will get called and exit the extension before you ever get a value from your Firebase database.
Try running .completeRequestReturningItems() in the andCompletionBlock.
override func didSelectPost() {
if self.sharedURL != nil {
// Send data to Firebase
self.myRootRef.runTransactionBlock({
(currentData:FMutableData!) in
var value = currentData.value as? String
// Getting the current value
// and checking whether it's null
if value == nil {
value = ""
}
// Setting the new value to the clipboard
// content
currentData.value = self.sharedURL?.absoluteString
// Finalizing the transaction
return FTransactionResult.successWithValue(currentData)
}, andCompletionBlock: {
// Completion Check
(error:NSError!, success:Bool, data:FDataSnapshot!) in
self.extensionContext!.completeRequestReturningItems([], completionHandler: nil)
}
)
}
}