Swift 3: How to retry Firebase upload on failure - swift

I'm initializing some data in Firebase that I will use to track a user's activity. I need to make sure this data is written to Firebase so I'm wondering what the best practice is for ensuring a critical upload was successful?
static func createUserActivityCounts(uid: String) {
DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + .seconds(5)) {
let databaseReference = Database.database().reference()
let userCounts: [String: Any] = ["posts": 0,
"comments": 0,
"likes": 0]
databaseReference.child("userActivity").child(uid).child("counts").setValue(userCounts) { (error, ref) -> Void in
if error != nil {
print(error!.localizedDescription)
}
}
}
}

I think the best way to resolve this issue is to check if the value exists whenever you attempt to query a critical upload. If the value isn't there then you initialize it. Here is the function I wrote to handle this.
private func getUserActivityCounts() {
if let uid = Auth.auth().currentUser?.uid {
userRef.child("userActivity").child(uid).child("counts").observeSingleEvent(of: .value, with: { snap in
if !snap.exists() {
// create user counts
if let uid = Auth.auth().currentUser?.uid {
DatabaseFunctions.createUserActivityCounts(uid: uid)
}
}
if let counts = snap.value as? [String: Any] {
if let numberOfLikes = counts["likes"] as? Int, let commentCount = counts["comments"] as? Int, let postCount = counts["posts"] as? Int {
DispatchQueue.main.async {
self.headerRef.postCount.text = String(postCount)
self.headerRef.commentCount.text = String(commentCount)
self.headerRef.likeCount.text = String(numberOfLikes)
self.collectionView.reloadData()
}
self.numberOfUserPosts = postCount
self.commentCount = commentCount
self.likeCount = numberOfLikes
}
}
})
}
}

Related

Restart Firebase observe

I created a method where I fetch all the published Books in Firebase. Each Book object stores a userId string value. I would like to fetch all books excluding currentUser books. I fetch 5 books every time I call the method starting from lastBookId, however if a user publishes more than 5 books, and are the first five, they can not be shown and as a result can not continue fetching them. I was thinking about increasing the limit value and calling the query observation again.
My code:
public func fetchBooksStarting(with lastBookId: String? = nil, completion: #escaping (Result<[Book], Error>) -> Void) {
var limit: UInt = 5
var books = [Book]()
let group = DispatchGroup()
var query = database.child("books").queryOrdered(byChild: "type")
if lastBookId != nil {
query = query.queryStarting(afterValue: BookType.Fiction.rawValue, childKey: lastBookId)
} else {
query = query.queryEqual(toValue: BookType.Fiction.rawValue)
}
query.queryLimited(toFirst: limit).observeSingleEvent(of: .value, with: { snap in
guard let snapshot = snap.children.allObjects as? [DataSnapshot] else {
completion(.failure(DatabaseErrors.failedToFetch))
return
}
books.removeAll()
for data in snapshot {
group.enter()
if let dict = data.value as? [String: AnyObject] {
let book = Book(dict: dict, bookId: data.key)
if book.userId == currentUserUid {
limit += 1
// recall query observe
} else {
books.append(book)
}
}
group.leave()
}
group.notify(queue: .main) {
completion(.success(books))
}
}, withCancel: nil)
}

Get data from firestore and assign it to an array of dictionaries

I am trying to get data from firestore collection and assign it to an array of dictionaries. for this part of the code below... i get the error "Cast from 'QuerySnapshot?' to unrelated type '[[String : Any]]' always fails" and the console prints "is not working".
guard let snap = snapshot as? [[String:Any]] else {
print("is not working")
completion(.failure(DatabaseError.failedToFetch))
return
}
Here is the full code.
// fetches and returns all conversations for the user with passed in uid
public func getAllConversations(for uid: String, completion: #escaping(Result<[Conversation], Error>) -> Void) {
print("fetching all convos")
//NEW
let db = Firestore.firestore()
let CurrentUser = Auth.auth().currentUser?.uid
let ListRef = db.collection("users").document(CurrentUser!).collection("conversations")
// fetch the current users convo list
ListRef.getDocuments { snapshot, error in
if let err = error {
debugPrint("Error fetching documents: \(err)")
} else {
guard let snap = snapshot as? [[String:Any]] else {
print("is not working")
completion(.failure(DatabaseError.failedToFetch))
return
}
print("is working")
let conversations: [Conversation] = snap.compactMap({ dictionary in
guard let id = dictionary["id"] as? String,
let name = dictionary["name"] as? String,
let otherUserUID = dictionary["other_user-uid"] as? String,
let latestMessage = dictionary["latest-message"] as? [String:Any],
let date = latestMessage["date"] as? String,
let message = latestMessage["message"] as? String,
let isRead = latestMessage["is-read"] as? Bool else {
return nil
}
//save other user ID to a global var
self.test = otherUserUID
//assign data into an array of dictionaries
let latestConvoObject = LatestMessage(date: date, text: message, isRead: isRead)
return Conversation(id: id, name: name, otherUserUid: otherUserUID, latestMessage: latestConvoObject)
})
completion(.success(conversations))
}
}
}
There are a numbers of way to read that data, and the process can be simplified by conforming objects to the codable protocol but let me provide a straight forward example. I don't know what your Conversation object looks like so here's mine
class ConversationClass {
var from = ""
var to = ""
var msg = ""
var timestamp = 0
convenience init(withDoc: DocumentSnapshot) {
self.init()
self.from = withDoc.get("from") as? String ?? "no from"
self.to = withDoc.get("to") as? String ?? "no to"
self.msg = withDoc.get("msg") as? String ?? "no msg"
self.timestamp = withDoc.get("timestamp") as? Int ?? 0
}
}
and then here's the the code that reads in all the conversation documents from a Collection, stores each in a ConversationClass object, puts those in an array and returns it through an escaping completion handler
func getConversations(completion: #escaping( [ConversationClass] ) -> Void) {
let conversationCollection = self.db.collection("conversations")
conversationCollection.getDocuments(completion: { snapshot, error in
if let err = error {
print(err.localizedDescription)
return
}
guard let docs = snapshot?.documents else { return }
var convoArray = [ConversationClass]()
for doc in docs {
let convo = ConversationClass(withDoc: doc)
convoArray.append(convo)
}
completion(convoArray)
})
}

Firebase items returning multiple times in collection view on database change

When the function to create or delete a list is called inside of my app the remaining lists are duplicated and displayed multiple times within the collectionView until the app is reloaded. I only called the fetchLists function twice, in the viewDidLoad and in the pull to refresh function. On pull to refresh the lists return to normal.
Fetch list:
fileprivate func fetchLists() {
self.collectionView?.refreshControl?.endRefreshing()
guard let currentUid = Auth.auth().currentUser?.uid else { return }
let ref = Database.database().reference().child("list-feed").child(currentUid)
ref.observe(.value) { (listFeedSnapshot) in
guard var allObjects = listFeedSnapshot.children.allObjects as? [DataSnapshot] else { return }
allObjects.forEach({ (allObjectsSnapshot) in
let listId = allObjectsSnapshot.key
let listRef = Database.database().reference().child("lists").child(listId)
listRef.observeSingleEvent(of: .value, with: { (snapshot) in
guard let dict = snapshot.value as? [String: Any] else { return }
guard let uid = dict["uid"] as? String else { return }
Database.fetchUserWithUID(uid: uid, completion: { (user) in
guard let dictionary = snapshot.value as? [String: Any] else { return }
var list = List(user: user, dictionary: dictionary)
let listId = snapshot.key
list.id = snapshot.key
self.list = list
self.lists.append(list)
self.lists.sort(by: { (list1, list2) -> Bool in
return list1.creationDate.compare(list2.creationDate) == .orderedDescending
})
self.collectionView?.reloadData()
ref.keepSynced(true)
listRef.keepSynced(true)
})
})
})
}
}
Create list:
let values = ["uid": uid, "title": listNameText, "creationDate": Date().timeIntervalSince1970] as [String : Any]
ref.updateChildValues(values, withCompletionBlock: { (error, ref) in
if let error = error {
self.navigationItem.rightBarButtonItem?.isEnabled = true
print("failed to save user info into db:", error.localizedDescription)
return
}
let memberValues = [uid : 1]
ref.child("list-members").updateChildValues(memberValues)
self.handleUpdateFeeds(with: ref.key!)
self.handleListFeeds(with: ref.key!)
print("successfully created list in db")
Update feeds:
func handleUpdateFeeds(with listId: String) {
guard let uid = Auth.auth().currentUser?.uid else { return }
let values = [listId: 1]
Database.database().reference().child("list-feed").child(uid).updateChildValues(values)
}
func handleListFeeds(with listId: String) {
guard let uid = Auth.auth().currentUser?.uid else { return }
let values = [listId: 1]
Database.database().reference().child("user-lists").child(uid).updateChildValues(values)
}
Firebase database:
{
"list-feed" : {
"otxFDz0FNbVPpLN27DYBQVP4e403" : {
"-LjeAoHJTrYK7xjwcpJ9" : 1,
"-LjeApq-Mb_d_lAz-ylL" : 1
}
},
"lists" : {
"-LjeAoHJTrYK7xjwcpJ9" : {
"creationDate" : 1.5630020966384912E9,
"title" : "Test 1",
"uid" : "otxFDz0FNbVPpLN27DYBQVP4e403"
},
"-LjeApq-Mb_d_lAz-ylL" : {
"creationDate" : 1.563002101329072E9,
"list-members" : {
"otxFDz0FNbVPpLN27DYBQVP4e403" : 1
},
"title" : "Test 2",
"uid" : "otxFDz0FNbVPpLN27DYBQVP4e403"
}
}
}
Since you're calling ref.observe(, you're attaching a permanent observer to the data. This means that if you call fetchLists a second, you're attaching a second observer and you'll get the same data twice.
If you only want the data to be read once per call to fetchLists, you should use observeSingleEventOfType:
fileprivate func fetchLists() {
self.collectionView?.refreshControl?.endRefreshing()
guard let currentUid = Auth.auth().currentUser?.uid else { return }
let ref = Database.database().reference().child("list-feed").child(currentUid)
ref.observeSingleEvent(of: .value) { (listFeedSnapshot) in
Also see the documentation on reading data once.

Cant fetch SavedPost

My app has an option to save posts for users to watch later. The code is:
func savedPost(for cell: FirstView) {
guard let indexPath = collectionView.indexPath(for: cell) else { return }
var post = self.posts[indexPath.item]
guard let currentuserId = Auth.auth().currentUser?.uid else { return }
let targetUid = post.user.uid
guard let postId = post.id else { return }
let ref = Database.database().reference().child("save_post").child(currentuserId).child(postId)
if post.hasSaved {
ref.removeValue { (err, _) in
if let _ = err {
showErr(info: NSLocalizedString("failtoUnsave", comment: ""), subInfo: tryLater)
return
}
post.hasSaved = false
self.posts[indexPath.item] = post
self.collectionView.reloadItems(at: [indexPath])
}
} else {
let values = ["userId": targetUid]
ref.updateChildValues(values) { (err, ref) in
if let _ = err {
showErr(info: NSLocalizedString("failtoSave", comment: ""), subInfo: tryLater)
}
post.hasSaved = true
self.posts[indexPath.item] = post
self.collectionView.reloadItems(at: [indexPath])
}
}
}
With this code my firebase database in "save_post" has -> currentUseruId -> postid -> postUserId.
On ProfileController users can view saved Posts from "savedPost" Tab. The code is:
var savedPosts = [Post]()
fileprivate func fetchSaved() {
var userIds = [String]()
var postIds = [String]()
guard let uid = self.user?.uid else { return }
let getIDsRef = Database.database().reference().child("save_post").child(uid)
let query = getIDsRef.queryOrderedByKey()
query.observeSingleEvent(of: .value) { (snapshot) in
let dictionary = snapshot.value as? [String: Any]
dictionary?.forEach({ (key,value) in
guard let dic = value as? [String: String] else { return }
postIds.append(key)
userIds.append(dic["userId"] ?? "")
})
var i = 0
while i < userIds.count {
self.fetchPostsWithUserIDPostID(userID: userIds[i], postID: postIds[i])
i += 1
}
}
}
fileprivate func fetchPostsWithUserIDPostID(userID: String, postID: String) {
let getPostRef = Database.database().reference().child("video_list")
getPostRef.child(userID).child(postID).observeSingleEvent(of: .value, with: { (snapshot) in
guard let dictionary = snapshot.value as? [String: Any] else { return }
let ref = Database.database().reference().child("users").child(userID)
ref.observeSingleEvent(of: .value, with: { (snapshot) in
guard let dict = snapshot.value as? [String: Any] else { return }
let user = User(uid: userID, dictionary: dict)
var post = Post(user: user, dictionary: dictionary)
post.id = postID
guard let currentUserUID = Auth.auth().currentUser?.uid else { return }
Database.database().reference().child("likes").child(postID).child(currentUserUID).observeSingleEvent(of: .value, with: { (snapshot) in
if let value = snapshot.value as? Int, value == 1 {
post.hasLiked = true
} else {
post.hasLiked = false
}
post.hasSaved = true
self.savedPosts.append(post)
self.savedPosts.sort(by: { (p1, p2) -> Bool in
return p1.creationDate.compare(p2.creationDate) == .orderedDescending
})
self.collectionView.reloadData()
})
})
})
}
However, when I click "savedPost" tab, there is no post shown. I don't know where my mistake is. I have all the necessary code under all override func collectionView(....). I believe the error should come from the code listed above. I am sincerely looking for help to resolve this issue. Thanks.
There could be a number of things going on here. It would be good to throw some print statements in there to make sure that 1) the data you're getting back from the database looks like what you expect, and 2) that you're properly parsing it into Post objects. Do you have your cells defined properly for your CollectionView? Also, I don't see where you are defining the data source for the CollectionView.

Synced reading from Firebase

I have a value I need to read from Firebase and then write it together with multiple other values to Firebase in a transaction of two objects total.
I am creating a CHAT and so when a message is sent, I am creating a chat room for both contacts, each to his own. My code :
private func CreateChatRoom(creatorID: String, creatorName: String ,contactID: String, contactName: String)
{
var creatorImageString: String = ""
var contactImageString: String = ""
ReadContactImage(contactID: contactID)
{
success in
if success
{
contactImageString = self.tempContactImg
}
}
ReadContactImage(contactID: creatorID)
{
success in
if success
{
creatorImageString = self.tempContactImg
}
}
let infoForCreator = [Constants.Chat.ChatRoomsLite.CONTACT_NAME: contactName,
Constants.Chat.ChatRoomsLite.CONTACT_ID: contactID,
Constants.Chat.ChatRoomsLite.NUM_OF_UNREAD_MSGS : 0,
Constants.Chat.ChatRoomsLite.CONTACT_IMG_URL: contactImageString] as [String : Any]
let infoForContact = [Constants.Chat.ChatRoomsLite.CONTACT_NAME: creatorName,
Constants.Chat.ChatRoomsLite.CONTACT_ID: creatorID,
Constants.Chat.ChatRoomsLite.NUM_OF_UNREAD_MSGS : 0,
Constants.Chat.ChatRoomsLite.CONTACT_IMG_URL: creatorImageString] as [String : Any]
let childUpdates = ["\(creatorID)/\(contactID)/": infoForCreator,
"\(contactID)/\(creatorID)/": infoForContact
]
Constants.refs.databaseChatsLite.updateChildValues(childUpdates)
}
private func ReadContactImage(contactID: String, completion: #escaping (Bool) -> ())
{
Constants.refs.databaseUsers.child(contactID).child(Constants.Account.AccountFields.USER_IMAGE_STR).observeSingleEvent(of: .value, with: {(snapshot) in
self.tempContactImg = (snapshot.value as? String)!
completion(true)
})
}
var tempContactImg : String = "";
I read here on SO that the function "ReadContactImage" should run synchronously, but it does not. So I'm left with empty contact images.
I thought about just reading both images in the same function, but CreateChatRoom also needs to be synchronous, so I am left with the same problem, basically.
Does anyone know how to handle this properly ?
Is there maybe an easier way of doing this?
Edit:
If writing to Database is async, I get an exception here:
func AddChatToCollections(chatAsDictionary: NSDictionary!)
{
if chatAsDictionary == nil
{
return
}
let contactName = chatAsDictionary[Constants.Chat.ChatRoomsLite.CONTACT_NAME] as! String
let contactImg = chatAsDictionary[Constants.Chat.ChatRoomsLite.CONTACT_IMG_URL] as! String
//let lastMsg = chatAsDictionary["lastMessage"] as! String
let newMsgs = chatAsDictionary[Constants.Chat.ChatRoomsLite.NUM_OF_UNREAD_MSGS] as! Int
let contactID = chatAsDictionary[Constants.Chat.ChatRoomsLite.CONTACT_ID] as! String
let chatToAdd = PrivateChatLiteObject(chattingWith: contactName, ContactID: contactID, unreadMessages: newMsgs, LastMSG: "", ContactImageStr: contactImg)
chatsDictionary[contactID] = chatToAdd
chatsIndex.append(contactID)
}
When trying to use the information in dictionary, which is taken from Firebase.
That function is called from here:
private func populateActiveChats()
{
let loggedOnUserID = Auth.auth().currentUser?.uid
let ref = Constants.refs.databaseChatsLite.child(loggedOnUserID!)
// Retrieve the products and listen for changes
ref.observe(.value, with:
{ (snapshot) in
for child in snapshot.children.allObjects as! [DataSnapshot]
{
if (self.chatsDictionary.keys.contains(child.key) == false)
{
let chatValueDictionary = child.value as? NSDictionary
self.AddChatToCollections(chatAsDictionary: chatValueDictionary)
self.DispatchQueueFunc()
}
}
})
}
Which is called from viewDidLoad() when I open my Chats page.
Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value
Because chatAsDictionary[CONTACT_NAME] doesn't exist, because when chatAsDictionary gets its data from Firebase, it is not yet written there from the async function
Both methods you call load data from Firebase asynchronously. You can't constructor infoForCreator (et al) until both calls to ReadContactImage have completed.
A simple way to do that is to nest the calls:
var creatorImageString: String = ""
var contactImageString: String = ""
ReadContactImage(contactID: contactID)
{
success in
if success
{
contactImageString = self.tempContactImg
ReadContactImage(contactID: creatorID)
{
success in
if success
{
creatorImageString = self.tempContactImg
let infoForCreator = [Constants.Chat.ChatRoomsLite.CONTACT_NAME: contactName,
Constants.Chat.ChatRoomsLite.CONTACT_ID: contactID,
Constants.Chat.ChatRoomsLite.NUM_OF_UNREAD_MSGS : 0,
Constants.Chat.ChatRoomsLite.CONTACT_IMG_URL: contactImageString] as [String : Any]
let infoForContact = [Constants.Chat.ChatRoomsLite.CONTACT_NAME: creatorName,
Constants.Chat.ChatRoomsLite.CONTACT_ID: creatorID,
Constants.Chat.ChatRoomsLite.NUM_OF_UNREAD_MSGS : 0,
Constants.Chat.ChatRoomsLite.CONTACT_IMG_URL: creatorImageString] as [String : Any]
let childUpdates = ["\(creatorID)/\(contactID)/": infoForCreator,
"\(contactID)/\(creatorID)/": infoForContact
]
Constants.refs.databaseChatsLite.updateChildValues(childUpdates)
}
}
}
}
Alternative, you can keep a counter:
var creatorImageString: String = ""
var contactImageString: String = ""
var completedCount = 0;
ReadContactImage(contactID: contactID)
{
success in
if success
{
contactImageString = self.tempContactImg
completedCount++
if completedCount == 2
{
createDatabaseNode(contactImageString, creatorImageString)
}
}
}
ReadContactImage(contactID: creatorID)
{
success in
if success
{
creatorImageString = self.tempContactImg
completedCount++
if completedCount == 2
{
createDatabaseNode(contactImageString, creatorImageString)
}
}
}
And createDatabaseNode is then a function that contains your code to populates the data structures and calls updateChildValues.