How do i get firestore document id in a generic method? - swift

i am new to swift and i am trying to implement generic method to fetch data from firestore.
func fetch<T: Codable>(query: Query, completion:#escaping (Result<[T]?, Swift.Error>) -> Void) {
var dataArray = [T]()
query.addSnapshotListener { (querySnapshot, error) in
guard let snapshot = querySnapshot else {
print("Error fetching snapshots: \(error!)")
completion(.failure(error!))
return
}
if snapshot.isEmpty {
completion(.success(nil))
return
}
snapshot.documentChanges.forEach { change in
var data: T!
do {
data = try FirestoreDecoder().decode(T.self, from: change.document.data())
} catch let error {
completion(.failure(error))
return
}
if (change.type == .added) {
// print("New data: \(change.document.data())")
dataArray.append(data)
}
}
completion(.success(dataArray))
}
but here i don't know how do i get the document id from change.document.data(). please help me out if any one knows. thank you.

Maybe these links can help
https://firebase.google.com/docs/reference/swift/firebasefirestore/api/reference/Classes/DocumentChange
https://firebase.google.com/docs/reference/swift/firebasefirestore/api/reference/Classes/QueryDocumentSnapshot
You have the property newIndex in change of type UInt or you can print the content of change.document.data() and see if there is a field with a name corresponding to an id. The data method contains the values of a document on your firestore console.
Then you can access the dictionary item as follows:
change.document().data()["id"] as? Int

Related

How can i retrieve data from a collection if the condition is set firestore swift

Here is my code pls help
My target is to retrieve a list of channels that contain uid within Subscriptions collection.
func getUserSubscriptions(uid: String) {
db.collection("Channels").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
//No documents found
return
}
for document in querySnapshot!.documents {
document.reference.collection("Subscribers").whereField("user_id", isEqualTo: uid).addSnapshotListener { (querySnapshot, error) in
guard (querySnapshot?.documents) != nil else {
//No documents found
return
}
//I want to get Channels that only contain uid withing it's Subscribers collection, but unfortunately it only gets the whole Channels, please help.
DispatchQueue.main.async {
self.channels = documents.map { d in
return Channels(id: d.documentID, channel_id: d["channel_id"] as! String?, channel_name: d["channel_name"] as? String ?? "", creator: d["creator"] as? String ?? "")
}
}
}
}
}
}
Pls help I got stuck here.

How to remove SnapShot listener (Firestore - Swift)

func addUserObserver(_ update: #escaping () -> Void) {
FriendSystem.system.USER_REF.addSnapshotListener { snapshot, error in
self.userList.removeAll()
guard error == nil else {
print("Error retreiving collection")
return
}
for document in snapshot!.documents {
let email = document.get("email") as! String
if email != Auth.auth().currentUser?.email! {
self.userList.append(User(userEmail: email, userID: document.documentID))
}
update()
}
}
}
I have added a listener but can't figure out how to disconnect/remove it. Thanks!
As shown in the documentation on detaching a listener, you need to keep the value you get back from addSnapshotListener:
var listener = FriendSystem.system.USER_REF.addSnapshotListener { snapshot, error in
...
And then later you can remove the listener with:
listener.remove()

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]

Problem retrieving data from Firestore Database

I am trying to retrieve data from a collection in my firestore database. But when print the retrieved data, it doesn't conform to the format I expected.
Here is my code to retrieve the data below :
let db = Firestore.firestore()
func loadEvents() -> [Event] {
var events = [Event]()
let decoder = JSONDecoder()
db.collection("Events").addSnapshotListener { QuerySnapshot, error in
guard let documents = QuerySnapshot?.documents else {
print("Error fetching documents: \(error!)")
return
}
for doc in documents {
let dict = doc.data()
if let data = try? JSONSerialization.data(withJSONObject: dict, options: []) {
let event = try? decoder.decode(Event.self, from: data)
events.append(event!)
print(events) // Prints stuff like [Envy.Event] ...
}
}
// let events = documents.map {$0.data()}
}
return events
}
On reaching the print(events) it prints stuff like [Envy.Event]
[Envy.Event, Envy.Event]
[Envy.Event, Envy.Event, Envy.Event]
[Envy.Event, Envy.Event, Envy.Event, Envy.Event] which is not suitable data format for manipulation. I don't even understand this format; Can someone help me out ?
you are printing it wrong . you are printing the whole model class you created you need to refer the object inside the class for eg.
print(events.name) // use the object name you have in your class
print(events.location) // use the object name you have in your class
To add the data into array
Example Code:
db.collection("Events").getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
let list = Event(name: document.get("yourKeyName") as? String,location: document.get("yourAnotherKeyName") as? String)
events.append(list)
print(event.name)
print(event.location)
}
}
}
Hope this helps

Sorting JSON when using Alamofire and SwiftyJSON

Morning all,
I've been following along the examples in the excellent iOS Apps With REST APIs book and as a result using Alamofire and SwiftyJSON. One thing I don't see mentioned in the book is how you would sort the incoming json objects into a specific order, eg. date. The code I've pasted below works fine for pulling in the json objects however as far as I can tell, they're automatically ordered by created_by. I'd like to sort by a different order, lets say my class was called Vegetable and had a name attribute so that I could sort by something like:
.sort { $0.name < $1.name }
I'll start with the Vegetable class in Vegetable.swift
class Vegetable: ResponseJSONObjectSerializable {
var id: Int?
var name : String?
var date: NSDate?
}
Inside my JSONSerializer file I have the following, I'm not sure I'd wish to change the order directly in here as I'd prefer some more flexibility with each call.
public func responseArray<T: ResponseJSONObjectSerializable>(completionHandler: Response<[T], NSError> -> Void) -> Self {
let serializer = ResponseSerializer<[T], NSError> { request, response, data, error in
guard error == nil else {
return .Failure(error!)
}
guard let responseData = data else {
let failureReason = "Array could not be serialized because input data was nil."
let error = Error.errorWithCode(.DataSerializationFailed, failureReason: failureReason)
return .Failure(error)
}
let JSONResponseSerializer = Request.JSONResponseSerializer(options: .AllowFragments)
let result = JSONResponseSerializer.serializeResponse(request, response, responseData, error)
switch result {
case .Success(let value):
let json = SwiftyJSON.JSON(value)
var objects: [T] = []
for (_, item) in json {
if let object = T(json: item) {
objects.append(object)
}
}
return .Success(objects)
case .Failure(let error):
return .Failure(error)
}
}
return response(responseSerializer: serializer, completionHandler: completionHandler)
}
Then, in my APIManager I have the following function
func getAllVegetables(completionHandler: (Result<[Vegetable], NSError>) -> Void) {
Alamofire.request(VegetableRouter.GetVegetables())
.responseArray { (response:Response<[Vegetable], NSError>) in
completionHandler(response.result)
}
}
Finally, populate my tableview I have:
func loadVegetables() {
self.isLoading = true
VegetablesAPIManager.sharedInstance.getAllVegetables() {
result in
self.isLoading = false
if self.refreshControl.refreshing {
self.refreshControl.endRefreshing()
}
guard result.error == nil else {
print(result.error)
// TODO: Display Error
return
}
if let fetchedVegetables = result.value {
self.vegetables = fetchedVegetables
for vegetable in fetchedVegetables {
// Nothing here at the moment
}
}
self.tableView.reloadData()
}
}
I appreciate any help I can get with this, Thanks!
Since you have a NSDate property, you can sort with the compare method of NSDate.
let sorted = result.value.sort { $0.date.compare($1.date) == .OrderedAscending }