Swift & Firestore - appending dictionary to nested array using .arrayUnion() - ios

I am creating a chat application where a user can start multiple chats with a different person (just like the rest of the other chat apps). I'm using Swift with Cloud Firestore.
My database design is the following:
Chat collection: [
"chatMember": ["member1", "member2"],
"createdAt" : Date().timeIntervalSince1970,
"meesages" : []
]
One chat room will have 'messages' array and it will have message dictionaries aka an object.
Below code is where I am trying to append the message dictionary to the messages array.
The Firebase doc introduces .arrayUnion() <LINK to the document>.
But it gives me an error saying,
"Contextual type '[Any]' cannot be used with dictionary literal"
#IBAction func sendBtnPressed(_ sender: UIButton) {
if let messageBody = messageField.text, let messageSender = Auth.auth().currentUser?.email {
db.collection("collectionName").document("docName").updateData([
// FieldValue.arrayUnion() does not work for this case.
K.FB.messages: FieldValue.arrayUnion([
"sender": messageSender,
"content": messageBody,
"createdAt": Date().timeIntervalSince1970
])
]) { ... closure }
}
}
I can't find any information specifically related to Swift's appending Dictionary to nested array in the Cloud Firestore.
I found a very similar case on YouTube <https://youtu.be/LKAXg2drQJQ> but this is made with Angular which uses an object with { ...curly bracket}. FieldValue.arrayUnion() seems to work on other languages but not Swift.
It'd be awesome if someone who has resolved this issue would help me out.
Thank you in advance.

So basically what you're saying with this code:
K.FB.messages: FieldValue.arrayUnion([
"sender": messageSender,
"content": messageBody,
"createdAt": Date().timeIntervalSince1970
])
is that you want to append dictionary to array with name that is in K.FB.messages constant. But you don't really want to do that and it isn't even possible. You want to append array of dictionaries to array. This means that you must enclose your dictionary in a square brackets. Like shown below:
K.FB.messages: FieldValue.arrayUnion([[
"sender": messageSender,
"content": messageBody,
"createdAt": Date().timeIntervalSince1970
]])

Related

How to correctly send array with objects in JSON Swift iOS

Hello I am beginner and trying to understand how to send an array with objects. Does the server understand what an array is like Int, Strings or Booleans? Do you have to send the array in a string for JSON? What I do not understand?
var productsResult = ""
let encoder = JSONEncoder()
let productObject = ProductUsed(name: "Custom name", reason: "This Reason", apply_way: "Intravenous infusion", dosing: "2x", date_start: "22-02-1999", date_end: "22-03-2003")
do {
let result = try encoder.encode([productObject])
if let resultString = String(data: result, encoding: .utf8) {
print(resultString)
productsResult = resultString
}
} catch {
print(error)
}
json["products_used"] = productsResult
And I sent to server with parameters like this:
parameters: ["pregnancy_week": 0, "body_height": 198, "initials": "John Appleseed", "heavy_effect": false, "sex": "Male", "pregnancy": false, "month_of_birth": 3, "reaction": "No option checked", "additional_info": "Eeee", "products_used": "[{\"date_end\":\"22-03-2003\",\"dosing\":\"2x\",\"date_start\":\"22-02-1999\",\"apply_way\":\"Intravenous infusion\",\"name\":\"Custom name\",\"reason\":\"This Reason\"}]", "description": "Eeee", "result": "Recovery without lasting consequences", "year_of_birth": 1983, "day_of_birth": 11, "issue_date": "15-11-2020", "body_weight": 78]
but printed "resultString" in log and looks good...
[{"date_end":"22-03-2003","dosing":"2x","date_start":"22-02-1999","apply_way":"Intravenous infusion","name":"Custom name","reason":"This Reason"}]
What's wrong in my code and why I have " \ " between words in "products_used" key?
JSON, unlike XML, does not specify the structure and type explicitly. This means that the server must know what JSON data to expect.
In JSON there are a few value types (https://www.w3schools.com/js/js_json_syntax.asp):
a string
a number
an array
a boolean
null
a JSON object (a dictionary with tags like { "first" : "John", "last" : "Doe" }). This allows nesting.
A JSON object is a set of tag-value pairs. The tag and value are separated by : and pairs are separated by ,.
An array is a list of JSON values. So for example [ "hello", world" ] is a JSON array with 2 strings and [ 12, 54 ] is a JSON array with two numbers.
Your parameter list ["pregnancy_week": 0, "body_height": 198, ... is not an array but a dictionary instead. A Swift dictionary is translated to an JSON object, not a JSON array.
The \ you see printed acts as escape character. This escape character is used to allow you to have a " inside the string.
That's just a few things that I hope will help understand things a bit better. Your questions are pretty basic, which is fine and it's great you want to understand things. But instead of us explaining everything here, I think it would be best if you'd read about the structure of JSON and how JSON works in Swift on your own.

How do I create a new collection?

I'm building a chat app where there are GroupConversations and GroupMessages. Following the example of some of the Firestore YouTube videos, I've chosen to structure my data this way:
The ID of the GroupConversation is the ID for the collection of GroupMessages associated with that GroupConversation. Then inside of that document should be a new collection called Messages which holds documents.
Does that make sense or am I overcomplicating things? I can't seem to create that Messages collection or set anything to it in my Swift code
GroupConversations
id
title
desc
memberIds
GroupMessages
GroupConversationID
createdAt: Date // just because I need a key and a document?
Messages (new collection)
Message1
Message2
Message3
Thanks
Edit: Added Swift code
let db = Firestore.firestore()
var data: [String: Any] = [
"text": title,
"senderUsername": username,
"createdAt": Date(),
"updatedAt": Date()
]
let creationData: [String: Any] = [
"text": "Group Created",
"createdAt": Date()
]
let doc = db.collection("GroupMessages").addDocument(data: creationData)
db.document("GroupMessages/\(groupConvo.documentId)/\(doc.documentID)/messages").setData(data) { error in
if error == nil {
completion()
}
}
In my console, I'm seeing a new GroupMessage object created but not sub-collection of that GroupMessage called messages
You're not building the path to the document correctly. Document paths always alternate between collection and document names. To build a path to a document in a subcollection, it would go collection/document/subcollection/document. It looks like you've mixed up the last two elements in the string you're building.
In my opinion, it's harder to go wrong if you build up the document using methods rather than trying to build a string. Something like this:
db
.collection("GroupMessages")
.document(groupConvo.documentId)
.collection("Messages")
.document(doc.documentId)
.setData(...)

Get a value from a dictionary given the key of a different field

I'm using the third-party library SwiftAddressBook to work with Contacts in my app. I want to iterate through contacts in my device and extract a few specific values. Namely the twitter and facebook usernames if they exist.
SwiftAddressBook has an object called SwiftAddressBookPerson which represents a single contact. And SwiftAddressBookPerson object has a property called socialProfiles which contains an array of dictionaries. Each dictionary contains info about available social media account such as the username, url etc. Printing the value of socialProfiles looks like this.
[
SwiftAddressBook.MultivalueEntry<Swift.Dictionary<SwiftAddressBook.SwiftAddressBookSocialProfileProperty, Swift.String>>(value: [
SwiftAddressBook.SwiftAddressBookSocialProfileProperty.url: "http://twitter.com/isuru",
SwiftAddressBook.SwiftAddressBookSocialProfileProperty.username: "isuru",
SwiftAddressBook.SwiftAddressBookSocialProfileProperty.service: "twitter"],
label: nil, id: 0),
SwiftAddressBook.MultivalueEntry<Swift.Dictionary<SwiftAddressBook.SwiftAddressBookSocialProfileProperty, Swift.String>>(value: [
SwiftAddressBook.SwiftAddressBookSocialProfileProperty.url: "http://www.facebook.com/isuru",
SwiftAddressBook.SwiftAddressBookSocialProfileProperty.username: "isuru",
SwiftAddressBook.SwiftAddressBookSocialProfileProperty.service: "facebook"],
label: Optional("facebook"), id: 1)
]
I cleaned it up a little by doing this socialProfiles.map { $0.map { $0.value } } which outputs the following.
[
[
SwiftAddressBook.SwiftAddressBookSocialProfileProperty.url: "http://twitter.com/isuru",
SwiftAddressBook.SwiftAddressBookSocialProfileProperty.username: "isuru",
SwiftAddressBook.SwiftAddressBookSocialProfileProperty.service: "twitter"
],
[
SwiftAddressBook.SwiftAddressBookSocialProfileProperty.url: "http://www.facebook.com/isuru",
SwiftAddressBook.SwiftAddressBookSocialProfileProperty.username: "isuru",
SwiftAddressBook.SwiftAddressBookSocialProfileProperty.service: "facebook"
]
]
What I want now is given the service's name (twitter, facebook), retrieve the username used for that service.
How do I parse through the dictionary and get the value of a different field?
Given that this is an array, I'd use a first { $0.service == serviceName }.
Alternatively, you could loop over the entire array ahead of time to create a dictionary with key as serviceName, and value as the profile property. The advantage of this approach is subsequent lookups would be much faster.
I have named your array of swift address book dictionaries, profiles. The forced unwrapping in the flatMap call is safe because both key and value are tested for non-nil in the previous filter call.
The result is a single dictionary containing username values for the services listed in your interestingServices array.
let service = SwiftAddressBook.SwiftAddressBookSocialProfileProperty.service
let username = SwiftAddressBook.SwiftAddressBookSocialProfileProperty.username
let interestingServices = ["facebook", "twitter"]
let usernames = profiles
.filter {
guard
let service = $0[service],
$0[username] != nil
else {
return false
}
return interestingServices.contains(service)
}
.flatMap { profile in
return [profile[service]!, profile[username]!]
}

Swift, Firebase, Flashlight, how to store arrays?

I am currently trying to use Firebase, Flashlight and Swift to create an search function to retrieve a random object from my realtime database.
I am trying to perform the following query to Firebase at /search/request
var searchSettings : [Any] = []
if Settings.searchPackage != 99 {
searchSettings.append(["match" : Settings.searchPackage])
}
if Settings.searchCountry != .world {
if let region = Locale.current.regionCode {
searchSettings.append(["match" : region])
}
}
if Settings.searchGender != .All {
searchSettings.append(["match" : Settings.searchGender.rawValue])
}
let postData = [
"index" : "firebase",
"type" : "test",
"body" : [
"query" : searchSettings
]
] as [String : Any]
ref.setValue(postData, withCompletionBlock: { (error, reference) in
if error == nil {
FIRDatabase.database().reference().child("search/response").child(ref.key).observeSingleEvent(of: .childAdded, with: { (snapshot) in
if snapshot.exists() {
print("found random snapshot based on settings \(snapshot)")
}
})
}
})
The problem is, as Firebase described in the documentation it does currently support Arrays, therefor the content of "query" will be:
Flashlight will throw an error because it expects the "query" to contain "match" fields and not the indexes of an array of them.
How would I fix this? I want to be able to search based on multiple fields.
Technically, Flashlight doesn't care at all what you put inside the body tag; it simply passes it on to ElasticSearch. So if there is an error generated about the format, it's ElasticSearch that's doing the complaining.
What you're probably running into here is either a) ElasticSearch doesn't like that syntax, or Firebase's array-like behaviors are converting the array to an object.
Note that Flashlight will allow you to pass a JSON string in place of body. So if this is a result of the array-like behaviors, you can JSON.stringify() the query before passing it into ES, and it will come out the other end as intended.
If the problem is the ES syntax (as it appears to me) then you can simply run the queries directly against ES until they work, and then modify your client to submit correct syntax accordingly.
Take a look at this gist:
BodyQuery in Swift
I wrote this in my Android application project and I'm using this to build queries for ES. Here is equivalent in Java for Android ElasticSearch needs a json query, and you can create it easily using Maps (Android)/Dictionaries (iOS). Enjoy :)

How to push data with multiple types in Firebase?

I want to push an array that has strings, numbers and date. Do I have to update individually or is there another way I can accomplish this?
Example:
var categories : [String] = self.parseCategories()
var standbyDataStrings = [
"firstName":firstName,
"lastName":lastName,
"categories": categories,
"time_stamp":self.date.timeIntervalSince1970
]
var standbyDataNums = [
"radius":nf.numberFromString(self.helpRadiusLabel.text!),
"duration":nf.numberFromString(self.helpDurationLabel.text!)
]
standbyUserRef.updateChildValues(standbyDataStrings)
standbyUserRef.updateChildValues(standbyDataNums) // this gives me a error "string is not identical to NSObject"
Combining standByDataStrings and standbyDataNums gives me an error.
Or is there a way to retrieve a string from Firebase and using it as an int. It gets stored as a String with the quotations.
The Firebase API expects an NSDictionary for updateChildValues. A NSDictionary can't contain nil values. A normal Swift dictionary on the other hand can contain nil values.
The return type of numberFromString is NSNumber?, so Swift infers that the dictionary might contain nil and so it can't be passed to updateChildValues. By explicitly forcing non-nil values with ! you can make this code compile:
var standbyDataNums = [
"radius":nf.numberFromString(self.helpRadiusLabel.text!)!,
"duration":nf.numberFromString(self.helpDurationLabel.text!)!
]
standbyUserRef.updateChildValues(standbyDataNums) // now type checks

Resources