Working with asynchronous data with Swift closures and Firebase - swift

I need to call a specific Firebase reference and get back data. This operation will take place inside multiple VCs so I want to have a class where I will have various functions calling Firebase. For example, if I want to get all articles I will call my FirebaseHelpers class, and use the method/closure fetchArticles(). This way, if I want to refactor something I will only do it in FirebaseHelpers class, and not go through all VCs.
FirebaseHelpers
import UIKit
import Firebase
class FirebaseHelpers {
func fetchArticles(completion: #escaping ([Article]?, Error?) -> Void) {
var articles = [Article]()
let articlesQuery = Database.database().reference().child("articles").queryOrdered(byChild: "createdAt")
articlesQuery.observe(.value, with: { (snapshot) in
guard let articlesDictionaries = snapshot.value as? [String : Any] else { return }
articlesDictionaries.forEach({ (key, value) in
guard let articleDictionary = value as? [String: Any] else { return }
// build articles array
let article = Article(dictionary: articleDictionary)
print("this is article within closure \(article)")
articles.append(article)
})
})
completion(articles, nil)
}
}
In any viewController
let firebaseHelpers = FirebaseHelpers()
var articles = [Article]() {
didSet {
self.collectionView.reloadData()
}
}
// this is inside viewDidLoad()
firebaseHelpers.fetchArticles { (articles, error) in
guard let articles = articles else { return }
print("articles \(articles)")
self.articles = articles
}
The problem is that I don't get any results back. In my VC the print("articles (articles)") will return an empty array. But in my FirebaseHelpers fetchArticles() the print("this is article within closure (article)") will print the article(s) just fine.
Any idea why this is happening?
Thanks in advance.

You can move completion inside your asynchronous function:
class FirebaseHelpers {
func fetchArticles(completion: #escaping ([Article]?, Error?) -> Void) {
var articles = [Article]()
let articlesQuery = Database.database().reference().child("articles").queryOrdered(byChild: "createdAt")
articlesQuery.observe(.value, with: { (snapshot) in
guard let articlesDictionaries = snapshot.value as? [String : Any] else { return }
articlesDictionaries.forEach({ (key, value) in
guard let articleDictionary = value as? [String: Any] else { return }
// build articles array
let article = Article(dictionary: articleDictionary)
print("this is article within closure \(article)")
articles.append(article)
})
completion(articles, nil) // <- move here
})
// completion(articles, nil) // <- remove
}
}
Otherwise completion will be called before your asynchronous function.

Related

Chaining promises in Swift to initialize a custom object

I implemented PromiseKit in Swift to avoid callback hell with completion blocks. I need to know the best way to chain promises together to init custom objects that have other associated objects. For example a Comment object that has a User object attached to it.
First I fetch the comments from the DB, which all have a uid property in the DB structure. I ultimately want to end up with an array of comments, where each one has the correct user attached to it, so I can load both the comment and user data. This all seemed much easier with completion blocks, but I'm a total Promise noob so idk.
Here is the code in the controller that handles fetch
CommentsService.shared.fetchComments(withPostKey: postKey)
.then { comments -> Promise<[User]> in
let uids = comments.map({ $0.uid })
return UserService.shared.fetchUsers(withUids: uids)
}.done({ users in
// how to init Comment object with users now?
})
.catch { error in
print("DEBUG: Failed with error \(error)")
}
Here is comment fetch function:
func fetchComments(withPostKey postKey: String) -> Promise<[Comment]> {
return Promise { resolver in
REF_COMMENTS.child(postKey).observeSingleEvent(of: .value) { snapshot in
guard let dictionary = snapshot.value as? [String: AnyObject] else { return }
let data = Array(dictionary.values)
do {
let comments = try FirebaseDecoder().decode([Comment].self, from: data)
resolver.fulfill(comments)
} catch let error {
resolver.reject(error)
}
}
}
}
Here is fetch users function
func fetchUsers(withUids uids: [String]) -> Promise<[User]> {
var users = [User]()
return Promise { resolver in
uids.forEach { uid in
self.fetchUser(withUid: uid).done { user in
users.append(user)
guard users.count == uids.count else { return }
resolver.fulfill(users)
}.catch { error in
resolver.reject(error)
}
}
}
}
Here is comment object:
struct Comment: Decodable {
let uid: String
let commentText: String
let creationDate: Date
var user: User?
}
This is how simple it is with completion blocks, starting to think Promises aren't worth it?
func fetchComments(withPostKey postKey: String, completion: #escaping([Comment]) -> Void) {
var comments = [Comment]()
REF_COMMENTS.child(postKey).observe(.childAdded) { (snapshot) in
guard let dictionary = snapshot.value as? [String: AnyObject] else { return }
guard let uid = dictionary["uid"] as? String else { return }
UserService.shared.fetchUser(withUid: uid, completion: { (user) in
let comment = Comment(user: user, dictionary: dictionary)
comments.append(comment)
completion(comments)
})
}
}
Ok I think I see what you are trying to do. The issue is that you need to capture the comments along with the users so you can return then together and later combine them. It should look something like this:
CommentsService.shared.fetchComments(withPostKey: postKey)
.then { comments -> Promise<[Comment], [User]> in
let uids = comments.map({ $0.uid })
return UserService.shared.fetchUsers(withUids: uids)
.then { users in
return Promise<[Comment], [User]>(comments, users)
}
}.done({ combined in
let (comments, users) = combined
//Do combiney stuff here
})
.catch { error in
print("DEBUG: Failed with error \(error)")
}
The transforms are [Comment] -> [User] -> ([Comment], [User]) -> [Comments with users attached]

How do I get data out of a firebase snapshot in Swift using a function in a struct?

I'm getting data from Cloud Firestore to populate a ListView. I've managed to get the data into an array, but when I return the array, it shows up empty.
//Edit
I've implemented a completion handler, works perfectly for 'Test Code', but when called in 'func industryPosts' and passed into 'myArray', it returns nil. While 'Test Code' returns data. I'm new to completion handlers, and Swift in general. Kindly let me know what I'm missing. Thanks.
//Edit
I was not able to return the values, but calling industryPosts where I needed to use it worked!
import Foundation
import SwiftUI
class IndustryData {
var _snapshotArray : Array<Any>?
func getSnapshotArray(collectionRef: String, completionHandler: #escaping (Array<Any>?, NSError?) -> ()){
if let snapArray = self._snapshotArray {
completionHandler(snapArray, nil)
} else {
var snapArray : Array<Any> = []
db.collection(collectionRef).getDocuments { (snapshot, error) in
guard let snapshot = snapshot else {
print("Error - > \(String(describing: error))")
return
}
for document in snapshot.documents {
let item = Industry(avatar: document.get("avatar") as! String, name:document.documentID, tags: document.get("tags") as! String)
snapArray.append(item)
}
self._snapshotArray = snapArray
completionHandler(snapArray, error as NSError?)
}
}
}
}
Then calling the below function where needed
func getposts()-> [Industry] {
let data = IndustryData()
data.getSnapshotArray(collectionRef: "industry") { (snapshotArray, error) in
if snapshotArray != nil {
self.myArray = snapshotArray!
}
}
return myArray as! [Industry]
}
myArray returned Industry Array!

How to wait for Swift's URLSession to finish before running again?

Probably a stupid question, but I'm a beginner at this.
The below code is supposed to get book information from Google Books from a keyword search. It then goes through the results and checks if I have a matching ISBN in a Firebase database. It works, but currently can only search 40 books as that's the Google Books API maximum per search.
Fortunately, I can specify where to start the index and get the next 40 books to search as well. Unfortunately, I've been trying for hours to understand how the URLSession works. All the methods I've tried have shown me that the code after the URLSession block doesn't necessarily wait for the session to complete. So if I check if I've found any matches afterward, it might not even be done searching.
I suspect the answer is in completion handling, but my attempts so far have been unsuccessful. Below is my code with a URL setup to take various starting index values.
var startingIndex = 0
//encode keyword(s) to be appended to URL
let query = query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
let url = "https://www.googleapis.com/books/v1/volumes?q=\(query)&&maxResults=40&startIndex=\(startingIndex)"
URLSession.shared.dataTask(with: URL(string: url)!) { (data, response, error) in
if error != nil {
print(error!.localizedDescription)
}else{
let json = try! JSONSerialization.jsonObject(with: data!, options: .allowFragments) as! [String: AnyObject]
if let items = json["items"] as? [[String: AnyObject]] {
//for each result make a book and add title
for item in items {
if let volumeInfo = item["volumeInfo"] as? [String: AnyObject] {
let book = Book()
//default values
book.isbn13 = "isbn13"
book.isbn10 = "isbn10"
book.title = volumeInfo["title"] as? String
//putting all authors into one string
if let temp = volumeInfo["authors"] as? [String] {
var authors = ""
for i in 0..<temp.count {
authors = authors + temp[i]
}
book.author = authors
}
if let imageLinks = volumeInfo["imageLinks"] as? [String: String] {
book.imageURL = imageLinks["thumbnail"]
}
//assign isbns
if let isbns = volumeInfo["industryIdentifiers"] as? [[String: String]] {
for i in 0..<isbns.count {
let firstIsbn = isbns[i]
if firstIsbn["type"] == "ISBN_10" {
book.isbn10 = firstIsbn["identifier"]
}else{
book.isbn13 = firstIsbn["identifier"]
}
}
}
//adding book to an array of books
myDatabase.child("listings").child(book.isbn13!).observeSingleEvent(of: .value, with: { (snapshot) in
if snapshot.exists() {
if listings.contains(book) == false{
listings.append(book)
}
DispatchQueue.main.async { self.tableView.reloadData() }
}
})
myDatabase.child("listings").child(book.isbn10!).observeSingleEvent(of: .value, with: { (snapshot) in
if snapshot.exists() {
if listings.contains(book) == false{
listings.append(book)
}
DispatchQueue.main.async { self.tableView.reloadData() }
}
})
}
}
}
}
SVProgressHUD.dismiss()
}.resume()
Below is my revised code:
func searchForSale(query: String, startingIndex: Int) {
directionsTextLabel.isHidden = true
tableView.isHidden = false
listings.removeAll()
DispatchQueue.main.async { self.tableView.reloadData() }
SVProgressHUD.show(withStatus: "Searching")
//clear previous caches of textbook images
cache.clearMemoryCache()
cache.clearDiskCache()
cache.cleanExpiredDiskCache()
let url = "https://www.googleapis.com/books/v1/volumes?q=\(query)&&maxResults=40&startIndex=\(startingIndex)"
URLSession.shared.dataTask(with: URL(string: url)!) { (data, response, error) in
if error != nil {
print(error!.localizedDescription)
}else{
var needToContinueSearch = true
let json = try! JSONSerialization.jsonObject(with: data!, options: .allowFragments) as! [String: AnyObject]
if json["error"] == nil {
let totalItems = json["totalItems"] as? Int
if totalItems == 0 {
SVProgressHUD.showError(withStatus: "No matches found")
return
}
if let items = json["items"] as? [[String: AnyObject]] {
//for each result make a book and add title
for item in items {
if let volumeInfo = item["volumeInfo"] as? [String: AnyObject] {
let book = Book()
//default values
book.isbn13 = "isbn13"
book.isbn10 = "isbn10"
book.title = volumeInfo["title"] as? String
//putting all authors into one string
if let temp = volumeInfo["authors"] as? [String] {
var authors = ""
for i in 0..<temp.count {
authors = authors + temp[i]
}
book.author = authors
}
if let imageLinks = volumeInfo["imageLinks"] as? [String: String] {
book.imageURL = imageLinks["thumbnail"]
}
//assign isbns
if let isbns = volumeInfo["industryIdentifiers"] as? [[String: String]] {
for i in 0..<isbns.count {
let firstIsbn = isbns[i]
//checks if isbns have invalid characters
let isImproperlyFormatted = firstIsbn["identifier"]!.contains {".$#[]/".contains($0)}
if isImproperlyFormatted == false {
if firstIsbn["type"] == "ISBN_10" {
book.isbn10 = firstIsbn["identifier"]
}else{
book.isbn13 = firstIsbn["identifier"]
}
}
}
}
//adding book to an array of books
myDatabase.child("listings").child(book.isbn13!).observeSingleEvent(of: .value, with: { (snapshot) in
if snapshot.exists() {
if listings.contains(book) == false{
listings.append(book)
needToContinueSearch = false
}
DispatchQueue.main.async { self.tableView.reloadData() }
}
})
myDatabase.child("listings").child(book.isbn10!).observeSingleEvent(of: .value, with: { (snapshot) in
if snapshot.exists() {
if listings.contains(book) == false{
listings.append(book)
needToContinueSearch = false
}
DispatchQueue.main.async { self.tableView.reloadData() }
return
}
if startingIndex < 500 {
if needToContinueSearch {
let nextIndex = startingIndex + 40
self.searchForSale(query: query, startingIndex: nextIndex)
}
}
})
}
}
}
}else{
return
}
}
SVProgressHUD.dismiss()
}.resume()
//hide keyboard
self.searchBar.endEditing(true)
}
In your completion handler if any results have been returned you end with:
DispatchQueue.main.async { self.tableView.reloadData() }
to trigger reloading of your table with the updated information. At this same point is where you could determine of there may be more results and initiate the next asynchronous URL task. In outline your code might be:
let needToContinueSearch : Bool = ...;
DispatchQueue.main.async { self.tableView.reloadData() }
if needToContinueSearch
{ // call routine it initiate next async URL task
}
(If there is any reason to start the task from the main thread the if would be in the block.)
By not initiating the next search until after you've processed the results of the first you avoid having to deal with any issues of a subsequent callback trying to update your data at the same time as a previous one.
However if you find delaying the second search in this way is too slow you can investigate ways to overlap the operations, e.g. you might have the callback just pass the processing of the results to an async task on a serial queue (so that only one set of results is being processed at once) and initiate the next async URL task.
HTH
Declare a bool variable as isLoading and if that function is loading dont trigger urlsession. hope below sample will help you.
var isLoading : Bool = false
func loadMore(with pageCount: Int){
if isLoading { return }
isLoading = true
// call the network
URLSession.shared.dataTask(with: URL(string: "xxxxx")!) { (data, response, error) in
// after updating the data set isloding to false again
// do the api logic here
//
DispatchQueue.main.async {
// self.items = downloadedItems
self.tableView.reloadData()
self.isLoading = false
}
}.resume()
}

Is there a way to use my array of type Music, in another scope?

I'm attempting to print/dump and array of type Music outside of a function it's created in. I can successfully dump the musicItems array inside of the getMusicData function but when I set the musicItems array outside of the scope, it won't print anything. What am I doing wrong with the scope here? I have a feeling it's super simple but I just can't figure it out. Thanks in advance for taking the time to read this.
edit: It's giving me "0 elements" in the console when I attempt to dump the musicItems array in the ViewController class. Well, the function is in the same class as well so I guess I don't know what to call the first array. The parent array?
struct MusicResults: Decodable {
let results: [Music]?
}
struct Music: Decodable {
let trackName: String?
let collectionName: String?
let artworkUrl30: String?
}
class ViewController: UITableViewController, UISearchBarDelegate {
var musicItems: [Music] = []
#IBAction func musicButton(_ sender: UIBarButtonItem) {
getMusicData()
dump(musicItems)
}
Here is the function.
func getMusicData() {
var musicItems: [Music] = []
guard let searchTerm = searchString else {return}
let newString = searchTerm.replacingOccurrences(of: " ", with: "+", options: .literal, range: nil)
let jsonUrlString = "https://itunes.apple.com/search?media=music&term=\(newString)"
guard let url = URL(string: jsonUrlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else { return }
do {
let music = try JSONDecoder().decode(MusicResults.self, from: data)
for results in music.results! {
// print(results.trackName!)
musicItems.append(results)
}
//dump(musicItems)
self.musicItems = musicItems
// DispatchQueue.main.async {
// self.tableView.reloadData()
// }
} catch let jsonErr {
print("Error serializing json:", jsonErr)
}
}.resume()
}
Fixed Code
#IBAction func musicButton(_ sender: UIBarButtonItem) {
getMusicData {
music in
dump(music)
}
function:
func getMusicData(completionHandler: #escaping (_ music: [Music]) -> ()) {
...
let music = try JSONDecoder().decode(MusicResults.self, from: data)
for results in music.results! {
musicItems.append(results)
}
completionHandler(musicItems)
...
Your 'getMusicData' function is asynchronous which means that when it executes, it queues data task in a background queue and proceeds the execution and since there are no more institutions it simply returns control to its calling site - 'musicButton()' action, which in its turn executes the next instruction - prints the 'musicItems' array which might (and most likely, is) still not populated as the network call haven’t yet completed. One of the options that you have here is to pass a completion block to your 'getMusicData' function, that runs it after data task gets the results.
Another option is to use Property Observers
var musicItems: [Music] = [] {
didSet {
dump(self.musicItems)
/// This is where I would do the...
// DispatchQueue.main.async {
// self.tableView.reloadData()
// }
}
}
and then
func getMusicData() {
guard let searchTerm = searchString else { print("no search"); return }
let newString = searchTerm.replacingOccurrences(of: " ", with: "+", options: .literal, range: nil)
let jsonUrlString = "https://itunes.apple.com/search?media=music&term=\(newString)"
guard let url = URL(string: jsonUrlString) else { print("url error"); return }
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else { print(err ?? "unknown"); return }
do {
let music = try JSONDecoder().decode(MusicResults.self, from: data)
if let results = music.results {
self.musicItems.append(contentsOf: results)
}
} catch let jsonErr {
print("Error serializing json:", jsonErr)
}
}.resume()
}

iOS writing to Firebase leads to crash

I wonder if my code is thread safe, in tableView(_ tableView:, leadingSwipeActionsConfigurationForRowAt indexPath:) I create an action that accepts a friend request. The method is invoked from a blok of UIContextualAction(style: .normal, title: nil) { (action, view, handler) in }
The actual Firebase call is like this:
class func acceptInvite(uid: String, completion: #escaping (Bool)->Void) {
guard let user = currentUser else { completion(false); return }
usersRef.child(user.uid).child("invites").queryEqual(toValue: uid).ref.removeValue()
usersRef.child(user.uid).child("friends").childByAutoId().setValue(uid)
usersRef.child(uid).child("friends").childByAutoId().setValue(user.uid)
completion(true)
}
image from debug navigator
It would be great if someone had an explanation.
edit: I think the problem is in my async loop to get the userdata
class func get(type: String, completion: #escaping ([Friend])->Void) {
let usersRef = Database.database().reference().child("users")
guard let user = currentUser else { completion([]); return }
usersRef.child(user.uid).child(type).observe(.value){ (snapshot) in
guard let invitesKeyValues = snapshot.value as? [String: String] else { completion([]); return }
var optionalFriendsDictArray: [[String: Any]?] = []
let dispatchGroup = DispatchGroup()
for (_, inviteUID) in invitesKeyValues {
dispatchGroup.enter()
usersRef.child(inviteUID).observe(.value, with: { (snapshot) in
let friend = snapshot.value as? [String: Any]
optionalFriendsDictArray.append(friend)
dispatchGroup.leave()
})
}
dispatchGroup.notify(queue: DispatchQueue.global(), execute: {
let friends = optionalFriendsDictArray.flatMap({ (optional) -> Friend? in
Friend.init(userDictionary: optional)
})
completion(friends)
})
}
}
This problem really gets me thinking about Firebase usage. I could add more information about the user at the friends key of a user so you don't have to query all the user to populate a small list with a name and a photo.
But what about viewing your friends posts on your timeline, your definitely not going to copy every friends' post into the users object. ???
I solved this problem by fetching the data with an observe single event and using the childadded and childremoved observers for mutations.