I am expecting data for a document field to be updated via a setData (merge) write and it does not despite completing successfully. An updateData works, but I don't understand why setData doesn't work in this instance.
I am performing the following writes to Firestore and the following paths:
setData to firstCollection/uniqueDoc
batched setData to secondCollection/uniqueDoc1 (between 1-5 writes in batch)
transaction updateData to thirdCollection/uniqueDoc
No error prints out, all data and syntax appear correct, and #2 plus #3 writes reflect in the data successfully - #1 is the one that does not.
Any guidance would be appreciated.
Here's my code for #1...
func first() {
//oldData is not empty and is the same type as var updatedData
//oldData = [
// "a": ["name":"aName", "dateAdded": dateValue, "imageUrl": "aImageUrl"]
// "b": ["name":"bName", "dateAdded": dateValue, "imageUrl": "bImageUrl"]
// ]
guard let oldData = oldData else { return }
var updatedData: [String: NSDictionary] = [:]
updatedData = oldData
updatedData["a"] = nil
db.collection(firstCollection).document(uniqueDocName).setData(["data": updatedData],
merge: true) { error in
guard error == nil else {
print("Error:" , error)
return
}
self.runOtherFunc()
}
}
Related
I want get folders and images from Firebase storage. On this code work all except one moment. I cant append array self.collectionImages in array self.collectionImagesArray. I don't have error but array self.collectionImagesArray is empty
class CollectionViewModel: ObservableObject {
#Published var collectionImagesArray: [[String]] = [[]]
#Published var collectionImages = [""]
init() {
var db = Firestore.firestore()
let storageRef = Storage.storage().reference().child("img")
storageRef.listAll { (result, error) in
if error != nil {
print((error?.localizedDescription)!)
}
for prefixName in result.prefixes {
let storageLocation = String(describing: prefixName)
let storageRefImg = Storage.storage().reference(forURL: storageLocation)
storageRefImg.listAll { (result, error) in
if error != nil {
print((error?.localizedDescription)!)
}
for item in result.items {
// List storage reference
let storageLocation = String(describing: item)
let gsReference = Storage.storage().reference(forURL: storageLocation)
// Fetch the download URL
gsReference.downloadURL { url, error in
if let error = error {
// Handle any errors
print(error)
} else {
// Get the download URL for each item storage location
let img = "\(url?.absoluteString ?? "placeholder")"
self.collectionImages.append(img)
print("\(self.collectionImages)")
}
}
}
self.collectionImagesArray.append(self.collectionImages)
print("\(self.collectionImagesArray)")
}
//
self.collectionImagesArray.append(self.collectionImages)
}
}
}
If i put self.collectionImagesArray.append(self.collectionImages) in closure its works but its not what i want
The problem is caused by the fact that calling downloadURL is an asynchronous operation, since it requires a call to the server. While that call is happening, your main code continues so that the user can continue to use the app. Then when the server returns a value, your closure/completion handler is invoked, which adds the URL to the array. So your print("\(self.collectionImagesArray)") happens before the self.collectionImages.append(img) has ever been called.
You can also see this in the order that the print statements occur in your output. You'll see the full, empty array first, and only then see the print("\(self.collectionImages)") outputs.
The solution for this problem is always the same: you need to make sure you only use the array after all the URLs have been added to it. There are many ways to do this, but a simple one is to check whether your array of URLs is the same length as result.items inside the callback:
...
self.collectionImages.append(img)
if self.collectionImages.count == result.items.count {
self.collectionImagesArray.append(self.collectionImages)
print("\(self.collectionImagesArray)")
}
Also see:
How to wait till download from Firebase Storage is completed before executing a completion swift
Closure returning data before async work is done
Return image from asynchronous call
SwiftUI: View does not update after image changed asynchronous
I tried several times with various ways to assign the collected values of documents from firestore into an array. Unfortunately, I could't find a way to solve this issue. I attached the code that I recently tried to implement. It includes before Firestore closure a print statement which print the whole fetched values successfully. However, after the closure and I tried to print the same array and the result is an empty array.
I tried to implement this code
var hotelCities: [String] = []
func getCities() {
db.collection("Hotels").getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
var found = false
let documentDetails = document.data() as NSDictionary
let location = documentDetails["Location"] as! NSDictionary
let city = location["city"]!
if (self.hotelCities.count == 0) {
self.hotelCities.append(String(describing: city))
}
else{
for item in self.hotelCities {
if item == String(describing: city){
found = true
}
}
if (found == false){
self.hotelCities.append(String(describing: city))
}
}
}
}
print(self.hotelCities)
}
print(self.hotelCities)
}
That's actually the expected result, since data is loaded from Firestore asynchronously.
Once you call getDocuments(), the Firestore client goes of and connects to the server to read those documents. Since that may take quite some time, it allows your application to continue running in the meantime. Then when the documents are available, it calls your closure. But that means the documents are only available after the closure has been called.
It's easiest to understand this flow, by placing a few print statements:
print("Before starting to get documents");
db.collection("Hotels").getDocuments() { (querySnapshot, err) in
print("Got documents");
}
print("After starting to get documents");
When you run this code, it will print:
Before starting to get documents
After starting to get documents
Got documents
Now when you first saw this code, that is probably not the output your expected. But it completely explains why the print(self.hotelCities) you have after the closure doesn't print anything: the data hasn't been loaded yet.
The quick solution is to make sure that all code that needs the documents is inside of the close that is called when the documents are loaded. Just like your top print(self.hotelCities) statement already is.
An alternative is to define your own closure as shown in this answer: https://stackoverflow.com/a/38364861
First response in this I get two user i.e abc#gmail.com & xyz#gmail.com
[{
"email": "abc#gmail.com",
"type": "primary_email",
"linked_to": {
"_id": "DAS44564dasdDASd",
"image": null,
"company": null,
"designation": null,
"name": null
},
"active_platforms": [
"asd",
"qwe"
]
},
{
"email": "xyz#gmail.com",
"type": "primary_email",
"linked_to": {
"_id": "DAS44564dasdDASd",
"image": null,
"company": null,
"designation": null,
"name": null
},
"active_platforms": [
"asd",
"qwe"
]
}]
Now if abc#gmail.com is deleted if I do API call again then still I get abc#gmail.com in my object as it is not deleted from my realm. So how to handle such situation ?
// write request result to realm database
let entries = json["data"]
realm.beginWrite()
for (_, subJson) : (String, JSON) in entries {
let entry: AppUsers = Mapper<AppUsers>().map(JSONObject: subJson.dictionaryObject!)!
realm.add(entry, update: true)
}
do {
try realm.commitWrite()
} catch {
}
Update your logic as below. This is one of method to this.
Add one extra bool field to AppUsers model say 'active'. Update your code as below
// write request result to realm database
let entries = json["data"]
realm.beginWrite()
//Fetch all realm AppUsers objects
let allAppUsers = //TODO fetch all AppUsers objects here
for user in allAppUsers {
user.active = false
}
for (_, subJson) : (String, JSON) in entries {
let entry: AppUsers = Mapper<AppUsers>().map(JSONObject: subJson.dictionaryObject!)!
entry.active = true
realm.add(entry, update: true)
}
for user in allAppUsers {
if !user.active {
realm.delete(user)
}
}
do {
try realm.commitWrite()
} catch {
}
This sounds like an issue where the data in your Realm database has become stale as the contents no longer match what is on the server.
The Realm API realm.add(_, update: true) will update any objects that were passed to it, but simply not passing an object does not imply it should be deleted (More that you just didn't want to update it).
There's no way for Realm to automatically know if an object needs to be deleted. You'll need to be in charge of that logic yourself.
Since your mechanism for checking if an object is deleted is via its email address, you could capture the email addresses of each object you've updated, and then delete any other objects whose email address is not in there.
// write request result to realm database
realm.beginWrite()
let entries = json["data"]
var updatedEmails = [String]()
for (_, subJson) : (String, JSON) in entries {
let entry: AppUsers = Mapper<AppUsers>().map(JSONObject: subJson.dictionaryObject!)!
// Save the email we just processed
updatedEmails.append(entry.email)
realm.add(entry, update: true)
}
// Delete all objects not in the updated emails list
let realmEntries = realm.objects(AppUsers.self)
for entry in realmEntries {
if !updatedEmails.contains(entry.email) {
realm.delete(entry)
}
}
do {
try realm.commitWrite()
} catch {
}
If your REST API brings down all of your objects in their complete form each time, a much quicker solution would also be to simply empty the Realm file each time and just add the objects as new objects each time too.
https://github.com/daltoniam/JSONJoy-Swift
For example :
JSON1 = {
"message": "Sorry! Password does not match.",
"code": "4"
}
JOSN2 = {
"data": {
"id": 21
},
"message": "Signup Successful.",
"code": "1"
},
Here json key “data” is optional. Then how I can handle both response using the same model object??
JSONJoy natively sets not found elements to nil, you just have to declare them optional and then check for nil before using them.
From the docs
This also has automatic optional validation like most Swift JSON libraries.
//some randomly incorrect key. This will work fine and the property
will just be nil.
firstName = decoder[5]["wrongKey"]["MoreWrong"].string
//firstName is nil, but no crashing!
Here is my example that my be illustrative. I have a complex object set where my top level object (UserPrefs) has arrays of secondary objects (SmartNetworkNotification and SmartNotificationTime).
Note that notifications and times are both declared as optional. What I do is check for nil after attempting to parse the secondary object arrays. Without the nil check the attempt to iterate on the parsed list fails since its nil. With the nil check it just moves past it if its empty.
This works for me but isn't deeply tested yet. YMMV! Curious how others are handling it.
struct UserPrefs: JSONJoy {
var notifications: [SmartNetworkNotification]?
var times: [SmartNotificationTime]?
init(_ decoder: JSONDecoder) throws {
// Extract notifications
let notificationsJson = try decoder["notifications"].array
if(notificationsJson != nil){
var collectNotifications = [SmartNetworkNotification]()
for notificationDecoder in notificationsJson! {
do {
try collectNotifications.append(SmartNetworkNotification(notificationDecoder))
} catch let error {
print("Error.. on notifications decoder")
print(error)
}
}
notifications = collectNotifications
}
// Extract time of day settings
let timesJson = try decoder["times"].array
if(timesJson != nil){
var collectTimes = [SmartNotificationTime]()
for timesDecoder in timesJson! {
do {
try collectTimes.append(SmartNotificationTime(timesDecoder))
} catch let error {
print("Error.. on timesJson decoder")
print(error)
}
}
times = collectTimes
}
}
I have a makeRequest() method inside a UITableViewController with the following code:
func makeRequest() {
Alamofire.request(.GET, self.foursquareEndpointURL, parameters: [
//"VENUE_ID" : self.foursquareVenueID,
"client_id" : self.foursquareClientID,
"client_secret" : self.foursquareClientSecret,
"v" : "20140806"
])
.responseJSON(options: nil) { (_, _, data, error) -> Void in
if error != nil {
println(error?.localizedDescription)
} else if let data: AnyObject = data {
let jObj = JSON(data)
if let venue = jObj["response"]["venue"].dictionaryValue as [String: JSON]? {
self.responseitems = jObj
println("venue is: \(venue)")
}
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData() // Update UI
}
}
}
}
also keep in mind that I have a property var responseitems:JSON = []
println("venue is: \(venue)") prints a nice looking response to console, so I know that is working correctly...
I also have a custom UITableViewCell class with a bindData() method with the following code:
func bindData() {
println("VenueDetailHeaderCell data did set")
self.venueDetailTitleLabel.text = self.headerInfo?["name"].stringValue
let labelData = self.headerInfo?["name"].stringValue
println("labelData is: \(labelData)")
}
As you can see, I am attempting to set a UILabel's text to the ["name"].stringValue in the JSON response. However, when I println("labelData is: \(labelData)") I get console output of labelData is: Optional("") which is obviously empty.
Here's a screenshot of what I'm trying to grab
What am I doing wrong here and how can I grab the name of the venue and assign my UILabel to it?
UPDATE:
I tried the following code
let labelData = self.headerInfo?["name"].error
println("labelData is: \(labelData)")
And get a console output of: "Error Domain=SwiftyJSONErrorDomain Code=901 "Array[0] failure, It is not an array" UserInfo=0x7fd6d9f7dc10 {NSLocalizedDescription=Array[0] failure, It is not an array}" If that is of use to anyone. I am really confused here... Any ideas?
The problem is that the headerInfo value is an error JSON object, because you're trying to access a dictionary with an integer index.
Note that var responseitems:JSON = [] does not create an array object. SwiftyJSON has auto-assignment-constructors (I'm new to swift, so not sure what the correct swift terminology is)... see this initialiser in the SwiftyJSON.swift source code:
extension JSON: ArrayLiteralConvertible {
public init(arrayLiteral elements: AnyObject...) {
self.init(elements)
}
}
What this means is that when you do var responseitems:JSON = [] you are not creating an array, you are creating a JSON object that is constructed with an empty array using the above init method. Then when you do self.responseitems = jObj you are re-assigning that responseitems variable to a JSON object with a dictionary in it. Therefore self.responseitems[0] is invalid.
Also note that with SwiftyJSON, there is no such thing as an optional JSON object. I notice in your comment you say that you do var headerInfo:JSON? ... - it's not possible to have an optional JSON.
var headerInfo: JSON = nil
The above is possible - this uses another auto-initialiser that initialises a valid JSON object that represents the JSON null value.
So, how to fix it?
When you assign headerInfo do it like this:
let headerInfo = self.responseitems["response"]["venue"]
And now in bindData you can do:
self.venueDetailTitleLabel.text = self.headerInfo["name"].stringValue
Note that all of the above assumes Swift 1.2 and SwiftyJSON >= 2.2 - also after you've understood the above and corrected the issue, you will probably want to refactor the code a bit to reflect the corrected understanding of the data-model.
The way to parse is the next
element: JSONValue //Something with JSONValue
if let title = element["name"]?.string {
cell.title.text = title
}