How to use PromiseKit with Firebase in Swift? - ios

So I import PromiseKit and then try
FIRDatabase.database().reference().child("somechild").removeValue().then {
/////
}
Obviously, this doesn't work and I was wondering what am I missing to make promises work with Firebase, if its even possible. What I'm trying to accomplish is to remove four Firebase references all at once with a single catch method.
With nodeJs I would easily use:
Promise.all ([
someRef.remove(),
someRef.remove(),
someRef.remove(),
someRef.remove()
]).then (function({
}).catch({
//handle error
})
Is there a way to accomplish this in Swift at all?

you can wrap Firebase function with fulfill and reject
/// Get chat IDs of user conversations
///
/// - Returns: array of user chat IDs
private func getUserChatIds() -> Promise<[String]> {
return Promise { fulfill, reject in
let userChatIDsRef = Database.database().reference()
.child(FireDatabasePaths.UserInfoPath.rawValue)
.child(userID).child("chatIDs")
userChatIDsRef.observe(.childAdded, with: { snapshot in
if let chatIDdic = snapshot.value as? [String: AnyObject] {
let keys = Array(chatIDdic.keys)
fulfill(keys)
} else {
reject(FirebaseError.empty)
}
})
}
}

Related

How to properly loop through a Firebase Query with completion handlers

Problem:
I need to loop through a Firebase query call.
My loop is initiated before the Firebase call as it holds the uids needed.
for uid in following_uids {
}
What is the proper way to loop variable uid into the Firebase reference & query?
for uid in following_uids {
let fbRef = ref.child("users").child(uid).child("posts")
//query firebase call
}
Replication:
func getRecentPostsFromFollowers(following_uids: [String]) {
for uid in following_uids {// first loop
let fbRef = ref.child("users").child(uid).child("posts")
fbRef.queryOrderedByKey().queryLimited(toLast: 5).observeSingleEvent(of: .value, with: {snapshot in
if(snapshot.exists()){
let values = snapshot.children.compactMap { ($0 as? DataSnapshot)?.value }
for postData in values {
guard let restDict = (postData as AnyObject) as? [String: Any] else { continue }
//do with data
}
}
})
}//end first loop
print("Completion handler - Loop Done")
}
PSEUDO code:
func getFollowerUIDS(completion: #escaping (_ followers: [String]) -> Void) {
for uid in following_uids {
let fbRef = ref.child("users").child(uid).child("posts")
//query firebase call
}
completion(value)
}
Specs:
Xcode Version 14.2 (14C18)
iOS
I'm assuming you'd like to combine the results from all user posts and return them as a single array of posts.
When you call observeSingleEvent on your document reference, the second parameter is a type of closure. This is essentially just another function that the Firebase SDK will call when it has the data ready for you.
The idea is that: because it may take some time to fetch this data, we don't want to block the rest of your function/code from running while the network call is taking place.
This means you will likely see the end of the loop "Completion handler - Loop Done" called before your data is made available.
This means your getRecentPostsFromFollowers function will return before any of your closures are called with data.
In order to allow callers of your getRecentPostsFromFollowers function to know when the data is ready, you can add your own completion handler to provide this data back to them.
This is a closure that you will call when you know all the data is ready.
However, because there's multiple closures with data that we need to combine, we can use something like a DispatchGroup for this purpose.
We'll combine the posts in an allPosts variable and return the data when it's combined from all the requests.
We need to lock around the access to allPosts as observeSingleEvent's completion handler can run on any thread, and multiple threads could try to access this variable at once.
typealias PostData = [String: Any]
func getRecentPostsFromFollowers(following_uids: [String], completion: #escaping ([PostData]) -> Void) {
let group = DispatchGroup()
let lock = NSLock()
var allPosts = [PostData]()
for uid in following_uids {
let fbRef = ref.child("users").child(uid).child("posts")
group.enter()
fbRef.queryOrderedByKey().queryLimited(toLast: 5).observeSingleEvent(of: .value, with: {snapshot in
defer { group.leave() }
if(snapshot.exists()){
let values = snapshot.children.compactMap { ($0 as? DataSnapshot)?.value }
lock.lock()
defer { lock.unlock() }
for postData in values {
guard let restDict = (postData as AnyObject) as? PostData else { continue }
allPosts.append(restData)
}
}
})
}
group.notify(queue: .main) {
completion(allPosts)
}
}
Side Notes
Swift async/await is a more modern way to handle asynchronous-based tasks like this without some of the pitfalls in this solution, it's worth looking into.
Performing a separate Firestore query for each user to retrieve all their data is not very efficient.
This is because each query requires a separate network request and will cost you at least one document read per query made, regardless if there are results or not per-user.
You should consider structuring your data within your Firestore database so you can return all the data you require in a single query.
This may involve denormalizing some of your data to allow for more efficient queries.

Swift - Problem While Downloading Data From Firebase Firestore Asynchronously with DispatchGroup()

While getting every document's ID in the "events" collection where their EventStatus value is equal to 0 and storing them in a string array (documentIds), I tried to run code asynchronously with DispatchGroup() so when I returned "documentIds", I would return a non-empty and complete value.
But when I run the code as it is below, it froze and in fact it never ran in the getDocuments{} closure.
I tried to run getDocuments{} closure in DispatchQueue.global().async{} but it didn't work also.
func someFunction() -> [String] {
var documentIds : [String]!
var dispatchGroup = DispatchGroup()
dispatchGroup.enter()
Firestore.firestore().collection("events").whereField("EventStatus", isEqualTo: 0).getDocuments { (snap, error) in
if let error = error {
print(error.localizedDescription)
return
}
guard let snap = snap else { return }
documentIds = snap.documents.map({ (document) -> String in
return document.documentID
})
dispatchGroup.leave()
}
dispatchGroup.wait()
return documentIds
}
When it froze, firebase gave this error in the debug console:
"Could not reach Cloud Firestore backend. Backend didn't respond within 10 seconds.
This typically indicates that your device does not have a healthy Internet connection at the moment. The client will operate in offline mode until it is able to successfully connect to the backend."
Other than that, no error or some other feedback. Am I doing something wrong with DispatchGroup() or Firestore?
Thanks for your help in advance!
This is one of the cases where dispatchGroup is useless and causes many errors.
Since retrieving data from Firestore is async call, use completion handler for your method instead of returning value and get rid of dispatchGroup
func someFunction(completion: #escaping ([String]) -> Void) {
Firestore.firestore().collection("events").whereField("EventStatus", isEqualTo: 0).getDocuments { snap, error in
if let error = error {
print(error.localizedDescription)
return
}
guard let snap = snap else { return }
var documentIds = snap.documents { document in
return document.documentID
}
completion(documentIds)
}
}
then call your method with completion handler where you have access to received array of String
someFunction { documentIds in // name completion parameter of type `[String]`
... // assign some global array as `documentIds` and then reload data, etc.
}

Returning data from function in Firebase observer code block swift

I'm new to firebase and I want to know if is any possible way to return data in observer block. I have class ApiManager:NSObject and in this class I want to create all my firebase function that will return some kind of data from database. This is one of my function in this class
func downloadDailyQuote() -> [String:String] {
let reference = Database.database().reference().child("daily")
reference.observeSingleEvent(of: .value) { (snap) in
return snap.value as! [String:String] //I want to return this
}
return ["":""] //I don't want to return this
}
And if I now do something like let value = ApiManager().downloadDailyQuote(), value contains empty dictionary. Is any solution for that?
Update: When you call .observeSingleEvent, you call the method asynchronously. This means that the method will start working, but the response will come later and will not block the main thread. You invoke this method, but there is no data yet and therefore you return an empty dictionary.
If you use the completion block, then you will get the data as soon as the method action is completed.
func downloadDailyQuote(completion: #escaping ([String:String]) -> Void) {
let reference = Database.database().reference().child("daily")
reference.observeSingleEvent(of: .value) { (snap) in
if let dictionaryWithData = snap.value as? [String:String] {
completion(dictionaryWithData)
} else {
completion(["" : ""])
}
}
}

How to handle SignalProducer with ReactiveSwift and Firebase asynchronous method calls?

I am working on an iOS App with Swift 3 using ReactiveSwift 1.1.1, the MVVM + Flow Coordinator pattern and Firebase as a backend. I only recently started to adapt to FRP and I am still trying to figure out how to integrate new functionalities into my existing code base.
For instance, my model uses a asynchronous method from Firebase to download thumbnails from the web and I want to provide a SignalProducer<Content, NoError> to subscribe from my ViewModel classes and observe, if thumbnails have been downloaded, which then updates the UI.
// field to be used from the view-models to observe
public let thumbnailContentSignalProducer = SignalProducer<Content, NoError> { (observer, disposable) in
// TODO: send next content via completion below
}
// thumbnail download method
public func findThumbnail(bucketId: String, contentId: String) {
guard let userId = userService.getCurrentUserId() else {
debugPring("Error id")
return
}
let ref = self.storageThumbnail.reference()
let contentRef = ref
.child(userId)
.child(bucketId)
.child(FirebaseConstants.pathImages)
.child("\(contentId).jpg")
contentRef.data(withMaxSize: 1 * 1024 * 1024, completion: { (data, error) in
guard let data = data else {
debugPrint("Error download")
return
}
let content = Image(data: data)
content.id = contentId
content.userId = userId
content.bucketId = bucketId
// TODO: emit signal with content
// How to send the content via the SignalProducer above?
})
}
I have also tried something similar with Signal<Content, NoError>, whereas I used the Signal<Content, NoError>.pipe() method to receive a (observer, disposable) tuple and I saved the observer as a private global field to access it form the Firebase callback.
Questions:
Is this the right approach or am I missing something?
How do I emit the content object on completion?
UPDATE:
After some hours of pain, I found out how to design the SingalProducer to emit signals and to subscribe from the ViewModels.
Maybe the following code snippet will help also others:
// model protocol
import ReactiveSwift
import enum Result.NoError
public protocol ContentService {
func findThumbnail(bucketId: String, contentId: String)
var thumbnailContentProducer: SignalProducer<Content, NoError> { get }
}
// model implementation using firebase
import Firebase
import FirebaseStorage
import ReactiveSwift
public class FirebaseContentService: ContentService {
// other fields, etc.
// ...
private var thumbnailContentObserver: Observer<Content, NoError>?
private var thumbnailContentSignalProducer: SignalProducer<Content, NoError>?
var thumbnailContentProducer: SignalProducer<Content, NoError> {
return thumbnailContentSignalProducer!
}
init() {
thumbnailContentSignalProducer = SignalProducer<Content, NoError> { (observer, disposable) in
self.thumbnailContentObserver = observer
}
}
func findThumbnail(bucketId: String, contentId: String) {
guard let userId = userService.getCurrentUserId() else {
// TODO handle error
return
}
let ref = self.storageThumbnail.reference()
let contentRef = ref
.child(userId)
.child(bucketId)
.child(FirebaseConstants.pathImages)
.child("\(contentId).jpg")
contentRef.data(withMaxSize: 1 * 1024 * 1024, completion: { (data, error) in
guard let data = data else {
// TODO handle error
return
}
let content = Image(data: data)
content.id = contentId
content.userId = userId
content.bucketId = bucketId
// emit signal
self.thumbnailContentObserver?.send(value: content)
})
}
}
// usage from a ViewModel
contentService.thumbnailContentProducer
.startWithValues { content in
self.contents.append(content)
}
Maybe someone can verify the code above and say that this is the right way to do it.
I think you were on the right path when you were looking at using Signal with pipe. The key point is that you need to create a new SignalProducer for each thumbnail request, and you need a way to combine all of those requests into one resulting signal. I was thinking something like this (note this is untested code, but it should get the idea across):
class FirebaseContentService {
// userService and storageThumbnail defined here
}
extension FirebaseContentService: ReactiveExtensionsProvider { }
extension Reactive where Base: FirebaseContentService {
private func getThumbnailContentSignalProducer(bucketId: String, contentId: String) -> SignalProducer<Content, ContentError> {
return SignalProducer<Content, ContentError> { (observer, disposable) in
guard let userId = self.base.userService.getCurrentUserId() else {
observer.send(error: ContentError.invalidUserLogin)
return
}
let ref = self.base.storageThumbnail.reference()
let contentRef = ref
.child(userId)
.child(bucketId)
.child(FirebaseConstants.pathImages)
.child("\(contentId).jpg")
contentRef.data(withMaxSize: 1 * 1024 * 1024, completion: { (data, error) in
guard let data = data else {
observer.send(error: ContentError.contentNotFound)
return
}
let content = Image(data: data)
content.id = contentId
content.userId = userId
content.bucketId = bucketId
observer.send(value: content)
observer.sendCompleted()
})
}
}
}
class ThumbnailProvider {
public let thumbnailSignal: Signal<Content, NoError>
private let input: Observer<(bucketId: String, contentId: String), NoError>
init(contentService: FirebaseContentService) {
let (signal, observer) = Signal<(bucketId: String, contentId: String), NoError>.pipe()
self.input = observer
self.thumbnailSignal = signal
.flatMap(.merge) { param in
return contentService.reactive.getThumbnailContentSignalProducer(bucketId: param.bucketId, contentId: param.contentId)
.flatMapError { error in
debugPrint("Error download")
return SignalProducer.empty
}
}
}
public func findThumbnail(bucketId: String, contentId: String) {
input.send(value: (bucketId: bucketId, contentId: contentId))
}
}
Using ReactiveExtensionsProvider like this is the idiomatic way of adding reactive APIs to existing functionality via a reactive property.
The actual requesting code is confined to getThumbnailContentSignalProducer which creates a SignalProducer for each request. Note that errors are passed along here, and the handling and conversion to NoError happens later.
findThumbnails just takes a bucketId and contentId and sends it through the input observable.
The construction of thumbnailSignal in init is where the magic happens. Each input, which is a tuple containing a bucketId and contentId, is converted into a request via flatMap. Note that the .merge strategy means the thumbnails are sent as soon as possible in whatever order the requests complete. You can use .concat if you want to ensure that the thumbnails are returned in the same order they were requested.
The flatMapError is where the potential errors get handled. In this case it's just printing "Error download" and doing nothing else.

Firebase query using a list of ids (iOS)

I have an NSArray containing multiple ids. Is there a way in Firebase where I can get all the object with the ids in the array?
I am building a restaurant rating app which uses GeoFire to retrieve nearby restaurants. My problem is that GeoFire only returns a list of ids of restaurant that are nearby. Is there any way i can query for all the object with the ids?
No, you can't do a batch query like that in Firebase.
You will need to loop over your restaurant IDs and query each one using observeSingleEvent. For instance:
let restaurantIDs: NSArray = ...
let db = FIRDatabase.database().reference()
for id in restaurantIDs as! [String] {
db.child("Restaurants").child(id).observeSingleEvent(of: .value) {
(snapshot) in
let restaurant = snapshot.value as! [String: Any]
// Process restaurant...
}
}
If you are worried about performance, Firebase might be able to group all these observeSingleEvent calls and send them as a batch to the server, which may answer your original question after all ;-)
I know that this answer is considered accepted but I have had really good success using promise kit with the method frank posted with his javascript link Speed up fetching posts for my social network app by using query instead of observing a single event repeatedly and just wanted to share the swift version
So I have a list of users ids that are attached to a post like this:
also these methods are in my post class where I have access to the post id from firebase
// this gets the list of ids for the users to fetch ["userid1", "userid2"....]
func getParticipantsIds() -> Promise<[String]> {
return Promise { response in
let participants = ref?.child(self.key!).child("people")
participants?.observeSingleEvent(of: .value, with: { (snapshot) in
guard let snapshotIds = snapshot.value as? [String] else {
response.reject(FirebaseError.noData)
return
}
response.fulfill(snapshotIds)
})
}
}
// this is the individual query to fetch the userid
private func getUserById(id:String) -> Promise<UserData> {
return Promise { response in
let userById = dbRef?.child("users").child(id)
userById?.observeSingleEvent(of: .value, with: { (snapshot) in
guard let value = snapshot.value else {
response.reject(FirebaseError.noData)
return
}
do {
let userData = try FirebaseDecoder().decode(UserData.self, from: value)
response.fulfill(userData)
} catch let error {
response.reject(error)
}
})
}
}
// this is the where the magic happens
func getPostUsers(compeltion: #escaping (_ users:[UserData], _ error:Error?) -> ()){
getParticipantsIds().thenMap { (id) in
return self.getUserById(id: id)
}.done { (users) in
compeltion(users, nil)
}.catch({ error in
compeltion([], error)
})
}

Resources