How to get an array field from Firestore and write in struct field in Swift? - swift

The problem that I faced is, I can reach every field in Firestore and write in structs except array&map field.
My firestore data is something like:
let data : [String : Any] = [
"name" : "House A",
"price" : 2000,
"contents" : [
"water" : true,
"internet" : false
]
]
Here is getDocument function:
let docRef = db.collection("example").document("example")
docRef.getDocument { (document, error) in
if let document = document, document.exists {
let data = Houses(Doc: document)
...
...
...
} else {
print(error, "Item not found")
}
}
Here is my structs :
struct Houses {
var name: String?
var price: Int
var contents : Contents
init(Doc: DocumentSnapshot){
self.name = Doc.get("name") as? String ?? ""
self.price = Doc.get("price") as! Int
self.contents = Doc.get("contents") as! Contents
}
}
struct Contents {
var water: Bool
var internet : Bool
init?(data: [String: Any]) {
guard let water = data["water"] as? Bool,
let internet = data["internet"] as? Bool else {
return nil
}
self.water = water
self.internet = internet
}
}
The other version of Contents :
struct Contents {
var water: Bool
var internet : Bool
init(Doc: DocumentSnapshot){
self.water = Doc.get("water") as! Bool
self.internet = Doc.get("internet") as! Bool
}
}
UPDATED
The problem solved with changing this line:
self.contents = Doc.get("contents") as! Contents
to;
self.contents = Contents(data: Doc.get("contents") as! [String : Any])
name and price returns what I expected but contents always return nil. I tried to configure Contents but results are same. I think, I have to configure struct named Contents.
Any help would be appreciated.

Related

How to get Firebase data as a model in swift?

I am trying to get data from firebase and use it as a model that I created.
Here is my model;
class UserData{
var nickname : String = ""
var onesignal_player_id : String = ""
var step_count : Int = 0
var total_point : Int = 0
var competitions : [String:Competition] = [String:Competition]()
}
class Competition{
var end_date : String = ""
var gift : String = ""
var id: String = ""
var name: String = ""
var users : [String:Int] = [:]
}
and this is my function;
func getFirebaseData() {
ref = Database.database().reference()
ref.child("users").child("HXXNCXf6RRS4WVO12shZ3j15BnG3").observe(.value) { (snapshot) in
if let snapshotValue = snapshot.value as? Dictionary<String,Any> {
//change userData with the snapshotValue
self.userData.nickname = snapshotValue["nickname"] as! String
self.userData.step_count = snapshotValue["step_count"] as! Int
self.userData.total_point = snapshotValue["total_point"] as! Int
self.userData.onesignal_player_id = snapshotValue["onesignal_player_id"] as! String
self.userData.competitions = snapshotValue["competitions"] as! [String:Competition]
//reload UI with userData
print(self.userData.competitions)
} else {
print("An error occured while assigning snapshotValue to userData")
}
}
}
This code gave me error like this;
Could not cast value of type '__NSDictionaryM' (0x1f47ada20) to 'StepCounterApp.Competition' (0x1004c06f0).
2021-01-02 23:05:49.985711+0300 StepCounterApp[32511:3685645] Could not cast value of type '__NSDictionaryM' (0x1f47ada20) to 'StepCounterApp.Competition' (0x1004c06f0).
but when i comment out the line included self.userData.competitions from getFirebaseData function, everything works fine.
What should I do? How can I use firebase data as a model?
Finally here is my firebase data;
The problem is in your data model. Declare your model data like this
class UserData {
var nickname : String = ""
var onesignal_player_id : String = ""
var step_count : Int = 0
var total_point : Int = 0
var competitions : Competition = Competition()
}
class Competition{
var end_date : String = ""
var gift : String = ""
var id: String = ""
var name: String = ""
var users : [String:Int] = [:]
init() {
}
init(with dictionary: [String: Any]) {
self.end_date = dictionary["end_date"] as! String
self.gift = dictionary["gift"] as! String
self.id = dictionary["id"] as! String
self.name = dictionary["name"] as! String
self.users = dictionary["users"] as! [String:Int]
}
}
And inside the getFirebaseData funcation
self.userData.competitions = Competition(with: snapshotValue["competitions"] as! [String: Any])
The problem was in my data model and with the help of Raja Kishan's data model sugestion I fixed the problem.
First I changed the model little bit;
class UserData{
var nickname : String = ""
var onesignal_player_id : String = ""
var step_count : Int = 0
var total_point : Int = 0
var competitions : [String:Competition] = [String:Competition]()
}
class Competition{
var end_date : String = ""
var gift : String = ""
var id: Int = 0
var name: String = ""
var users : [String:Int] = [:]
init() {
}
init(with dictionary: [String: Any]) {
self.end_date = dictionary["end_date"] as! String
self.gift = dictionary["gift"] as! String
self.id = dictionary["id"] as! Int
self.name = dictionary["name"] as! String
self.users = dictionary["users"] as! [String:Int]
}
}
Than I add a childSnapshot to my method so I can work directly the "competitions";
func getFirebaseData() {
ref = Database.database().reference()
ref.child("users").child("HXXNCXf6RRS4WVO12shZ3j15BnG3").observe(.value) { [self] (snapshot) in
if let snapshotValue = snapshot.value as? [String:Any] {
//change userData with the snapshotValue
self.userData.nickname = snapshotValue["nickname"] as! String
self.userData.step_count = snapshotValue["step_count"] as! Int
self.userData.total_point = snapshotValue["total_point"] as! Int
self.userData.onesignal_player_id = snapshotValue["onesignal_player_id"] as! String
//******
//This part of the coded added for to solve the problem starting from here
let childSnap = snapshot.childSnapshot(forPath: "competitions")
if let childSnapValue = childSnap.value as? [String:Any] {
childSnapValue.forEach { (element) in
self.userData.competitions.updateValue(Competition(with: element.value as! [String:Any]), forKey: element.key)
}
} else {
print("something wrong with the childSnap")
}
//to here
//******
} else {
print("An error occured while assigning snapshotValue to userData")
}
}
}

How to map the nested data in a document from Firestore by Swift?

I have a document data structure on Firestore like this:
pic1
pic2
So there are 2 map-objects inside the document and a collection and a another document inside this document
Then I create 3 model swift files for this document
task:
struct task {
var Name: String
var Address: String
var Car: CarModel
car Price: PriceModel
var dictionary: [String:Any] {
return [
"Name" : Name,
"Address" : Address,
"Car" : CarModel,
"Price" : PriceModel
]
}
init?(data: [String:Any]) {
guard let Name = dictionary["Name"] as? String,
let Address = dictionary["Address"] as? String,
let Car = ditionary["car"] as? CarModel,
let Price = dictionary["price"] as? PriceModel else{
return nil
}
self.Name = Name
self.Address = Address
self.Car = Car
self.Price = Price
}
}
CarModel:
struct CarModel {
var brand: String
var model: String
var year: String
var dictionary: [String:Any] {
return [
"brand" : brand,
"model" : model,
"year" : year,
]
}
init?(data: [String:Any]) {
guard let brand = dictionary["brand"] as? String,
let model = dictionary["model"] as? String,
let year = ditionary["year"] as? String else{
return nil
}
self.brand = brand
self.model = model
self.year = year
}
}
PriceModel:
struct PriceModel {
var basic: Int
var extra: Int
var dictionary: [String:Any] {
return [
"basic" : basic,
"extra" : extra,
]
}
init?(data: [String:Any]) {
guard let basic = dictionary["basic"] as? Int,
let extra = ditionary["extra"] as? Int else{
return nil
}
self.basic = basic
self.extra = extra
}
}
Then download the data with this following code:
func loadDataFromFirestore(completion: #escaping (Bool) -> ()) {
var success: Bool = false
DispatchQueue.global(qos: .userInteractive).async {
let downloadGroup = DispatchGroup()
let colRef = db.collection("tasks")
downloadGroup.enter()
colRef.getDocuments() { (querySnapshot, error) in
if let error = error {
print("Error: \(error)")
return
}
for document in querySnapshot!.documents{
let result = document.data()
print (result)
if let data = task(data: result){
print(data)
}
}
success = true
downloadGroup.leave()
}
downloadGroup.wait()
DispatchQueue.main.async {
completion(success)
}
}
}
I can get the data with comment the CarModel and PriceModel, but if I uncomment these two, it will let my app crash!
So how could I get the map-object to adapt to my code?
And the second question is: How can I get the document inside a document's collection with this kind of code?

Synced reading from Firebase

I have a value I need to read from Firebase and then write it together with multiple other values to Firebase in a transaction of two objects total.
I am creating a CHAT and so when a message is sent, I am creating a chat room for both contacts, each to his own. My code :
private func CreateChatRoom(creatorID: String, creatorName: String ,contactID: String, contactName: String)
{
var creatorImageString: String = ""
var contactImageString: String = ""
ReadContactImage(contactID: contactID)
{
success in
if success
{
contactImageString = self.tempContactImg
}
}
ReadContactImage(contactID: creatorID)
{
success in
if success
{
creatorImageString = self.tempContactImg
}
}
let infoForCreator = [Constants.Chat.ChatRoomsLite.CONTACT_NAME: contactName,
Constants.Chat.ChatRoomsLite.CONTACT_ID: contactID,
Constants.Chat.ChatRoomsLite.NUM_OF_UNREAD_MSGS : 0,
Constants.Chat.ChatRoomsLite.CONTACT_IMG_URL: contactImageString] as [String : Any]
let infoForContact = [Constants.Chat.ChatRoomsLite.CONTACT_NAME: creatorName,
Constants.Chat.ChatRoomsLite.CONTACT_ID: creatorID,
Constants.Chat.ChatRoomsLite.NUM_OF_UNREAD_MSGS : 0,
Constants.Chat.ChatRoomsLite.CONTACT_IMG_URL: creatorImageString] as [String : Any]
let childUpdates = ["\(creatorID)/\(contactID)/": infoForCreator,
"\(contactID)/\(creatorID)/": infoForContact
]
Constants.refs.databaseChatsLite.updateChildValues(childUpdates)
}
private func ReadContactImage(contactID: String, completion: #escaping (Bool) -> ())
{
Constants.refs.databaseUsers.child(contactID).child(Constants.Account.AccountFields.USER_IMAGE_STR).observeSingleEvent(of: .value, with: {(snapshot) in
self.tempContactImg = (snapshot.value as? String)!
completion(true)
})
}
var tempContactImg : String = "";
I read here on SO that the function "ReadContactImage" should run synchronously, but it does not. So I'm left with empty contact images.
I thought about just reading both images in the same function, but CreateChatRoom also needs to be synchronous, so I am left with the same problem, basically.
Does anyone know how to handle this properly ?
Is there maybe an easier way of doing this?
Edit:
If writing to Database is async, I get an exception here:
func AddChatToCollections(chatAsDictionary: NSDictionary!)
{
if chatAsDictionary == nil
{
return
}
let contactName = chatAsDictionary[Constants.Chat.ChatRoomsLite.CONTACT_NAME] as! String
let contactImg = chatAsDictionary[Constants.Chat.ChatRoomsLite.CONTACT_IMG_URL] as! String
//let lastMsg = chatAsDictionary["lastMessage"] as! String
let newMsgs = chatAsDictionary[Constants.Chat.ChatRoomsLite.NUM_OF_UNREAD_MSGS] as! Int
let contactID = chatAsDictionary[Constants.Chat.ChatRoomsLite.CONTACT_ID] as! String
let chatToAdd = PrivateChatLiteObject(chattingWith: contactName, ContactID: contactID, unreadMessages: newMsgs, LastMSG: "", ContactImageStr: contactImg)
chatsDictionary[contactID] = chatToAdd
chatsIndex.append(contactID)
}
When trying to use the information in dictionary, which is taken from Firebase.
That function is called from here:
private func populateActiveChats()
{
let loggedOnUserID = Auth.auth().currentUser?.uid
let ref = Constants.refs.databaseChatsLite.child(loggedOnUserID!)
// Retrieve the products and listen for changes
ref.observe(.value, with:
{ (snapshot) in
for child in snapshot.children.allObjects as! [DataSnapshot]
{
if (self.chatsDictionary.keys.contains(child.key) == false)
{
let chatValueDictionary = child.value as? NSDictionary
self.AddChatToCollections(chatAsDictionary: chatValueDictionary)
self.DispatchQueueFunc()
}
}
})
}
Which is called from viewDidLoad() when I open my Chats page.
Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value
Because chatAsDictionary[CONTACT_NAME] doesn't exist, because when chatAsDictionary gets its data from Firebase, it is not yet written there from the async function
Both methods you call load data from Firebase asynchronously. You can't constructor infoForCreator (et al) until both calls to ReadContactImage have completed.
A simple way to do that is to nest the calls:
var creatorImageString: String = ""
var contactImageString: String = ""
ReadContactImage(contactID: contactID)
{
success in
if success
{
contactImageString = self.tempContactImg
ReadContactImage(contactID: creatorID)
{
success in
if success
{
creatorImageString = self.tempContactImg
let infoForCreator = [Constants.Chat.ChatRoomsLite.CONTACT_NAME: contactName,
Constants.Chat.ChatRoomsLite.CONTACT_ID: contactID,
Constants.Chat.ChatRoomsLite.NUM_OF_UNREAD_MSGS : 0,
Constants.Chat.ChatRoomsLite.CONTACT_IMG_URL: contactImageString] as [String : Any]
let infoForContact = [Constants.Chat.ChatRoomsLite.CONTACT_NAME: creatorName,
Constants.Chat.ChatRoomsLite.CONTACT_ID: creatorID,
Constants.Chat.ChatRoomsLite.NUM_OF_UNREAD_MSGS : 0,
Constants.Chat.ChatRoomsLite.CONTACT_IMG_URL: creatorImageString] as [String : Any]
let childUpdates = ["\(creatorID)/\(contactID)/": infoForCreator,
"\(contactID)/\(creatorID)/": infoForContact
]
Constants.refs.databaseChatsLite.updateChildValues(childUpdates)
}
}
}
}
Alternative, you can keep a counter:
var creatorImageString: String = ""
var contactImageString: String = ""
var completedCount = 0;
ReadContactImage(contactID: contactID)
{
success in
if success
{
contactImageString = self.tempContactImg
completedCount++
if completedCount == 2
{
createDatabaseNode(contactImageString, creatorImageString)
}
}
}
ReadContactImage(contactID: creatorID)
{
success in
if success
{
creatorImageString = self.tempContactImg
completedCount++
if completedCount == 2
{
createDatabaseNode(contactImageString, creatorImageString)
}
}
}
And createDatabaseNode is then a function that contains your code to populates the data structures and calls updateChildValues.

Fetch multi level node from Firebase

I am trying to fetch the "friends" from the node to be able to show them in UICollectionView afterwards. I now realized that I have to use a struct and place the Friends array inside. I am struggling now to understand how to fetch them into that array (you can see it at the bottom of the post). Data is stored in a firebase node. How can I grab the data and what would be the procedure to place it in UICollectionView afterwards? This is my function so far to retrieve.
UPDATE: (I think I am fetching correctly now but I don't get any results. Is there something that I should do in collection view? or what am I doing wrong?)
UPDATE: Here is my code for post fetching:
func fetchPosts3() {
ref.child("Users_Posts").child("\(unique)").queryOrderedByKey().observeSingleEvent(of: .value, with: { snapshot in
print(snapshot)
if snapshot.value as? [String : AnyObject] != nil {
let allPosts = snapshot.value as! [String : AnyObject]
self.posts.removeAll()
for (_, value) in allPosts {
if let postID = value["postID"] as? String,
let userIDDD = value["userID"] as? String
{
//ACCESS FRIENDS
ref.child("Users_Posts").child("\(unique)").child(postID).child("friends").queryOrderedByKey().observeSingleEvent(of: .value, with: { (snap) in
print("FRIENDS: \(snap.childrenCount)")
//var routine = self.postsWithFriends[0].friends
for friendSnap in snap.children {
if let friendSnapshot = friendSnap as? DataSnapshot {
let friendDict = friendSnapshot.value as? [String: Any]
let friendName = friendDict?["name"] as? String
let friendPostID = friendDict?["postID"] as? String
let postsToShow = PostWithFriends(id: userIDDD, friends: [Friend(friendName: friendName!, friendPostID: friendPostID!)])
self.postsWithFriends.append(postsToShow)
print("COUNTING: \(self.postsWithFriends.count)")
// then do whatever you need with your friendOnPost
}
}
})
}
}
//GET LOCATION
self.collectionView?.reloadData()
self.posts.sort(by: {$0.intervalPosts! > $1.intervalPosts!})
}
})
ref.removeAllObservers()
}
That's how the data looks at the database:
{
"-LN2rl2414KAISO_qcK_" : {
"cellID" : "2",
"city" : "Reading",
"date" : "2018-09-23 00:41:26 +0000",
"friends" : {
"UJDB35HDTIdssCtZfEsMbDDmBYw2" : {
"name" : "Natalia",
"postID" : "-LN2rl2414KAISO_qcK_",
"userID" : "UJDB35HDTIdssCtZfEsMbDDmBYw2"
},
"Vyobk7hJu5OGzOe7E1fcYTbMvVI2" : {
"name" : "Gina C",
"postID" : "-LN2rl2414KAISO_qcK_",
"userID" : "Vyobk7hJu5OGzOe7E1fcYTbMvVI2"
}
},
}
}
And this is my object that's stored into array
struct PostWithFriends {
var postID : String?
var friends: [Friend]
}
class Friend : NSObject {
var friendName: String?
var friendUserID: String?
var postID: String?
init(friendName: String, friendPostID: String) {
self.friendName = friendName
self.postID = friendPostID
}
}
Replace this
if let friend = snap.value as? [String : AnyObject] {
}
With this:
for friendSnap in snap.children {
if let friendSnapshot = friendSnap as? FIRDataSnapshot {
let friendOnPost = FriendOnPost()
let friendDict = friendSnapshot.value as? [String: Any]
friendOnPost.name = friendDict?["name"] as? String
friendOnPost.friendUserID = friendDict?["userID"] as? String
friendOnPost.postID = friendDict?["postID"] as? String
// then do whatever you need with your friendOnPost
}
}

Firebase: How to put data in a child that's already created with childbyAutoID

people in my app sometimes needs to update the status of something. Now can you choose of 2 things: The so called "Rollerbank" is still there or the "Rollerbank" is removed. The users can create a data ref. The id that will be created by childbyAutoID. Now is my question how to get the right child and update some childs with a value. My post:
class Post {
let ref: DatabaseReference!
var TypeControle: String = ""
var Stad: String = ""
var Tijd: String = ""
var TijdControle: String = ""
var TijdControleniet: String = ""
var Latitude: String = ""
var Longitude: String = ""
var Extrainformatie: String = ""
var Staater: String = ""
var Staaternietmeer: String = ""
init(TypeControle: String) {
self.TypeControle = TypeControle
ref = Database.database().reference().child("Rollerbanken").childByAutoId()
}
init(Stad: String){
self.Stad = Stad
ref = Database.database().reference().child("Rollerbanken").childByAutoId()
}
init(Tijd: String) {
self.Tijd = Tijd
ref = Database.database().reference().child("Rollerbanken").childByAutoId()
}
init(Latitude: String) {
self.Latitude = Latitude
ref = Database.database().reference().child("Rollerbanken").childByAutoId()
}
init(Longitude: String) {
self.Longitude = Longitude
ref = Database.database().reference().child("Rollerbanken").childByAutoId()
}
init(Extrainformatie: String) {
self.Extrainformatie = Extrainformatie
ref = Database.database().reference().child("Rollerbanken").childByAutoId()
}
init(Staater: String) {
self.Staater = Staater
ref = Database.database().reference().child("Rollerbanken").child("Controletest").childByAutoId()
}
init(Staaternietmeer: String) {
self.Staaternietmeer = Staaternietmeer
ref = Database.database().reference().child("Rollerbanken").childByAutoId()
}
init(TijdControle: String) {
self.TijdControle = TijdControle
ref = Database.database().reference().child("Rollerbanken").childByAutoId()
}
init(TijdControleniet: String) {
self.TijdControleniet = TijdControleniet
ref = Database.database().reference().child("Rollerbanken").childByAutoId()
}
init() {
ref = Database.database().reference().child("Rollerbanken").childByAutoId()
}
init(snapshot: DataSnapshot)
{
ref = snapshot.ref
if let value = snapshot.value as? [String : Any] {
TypeControle = value["TypeControle"] as! String
Stad = value["Stad"] as! String
Tijd = value["Tijd"] as! String
Latitude = value["Latitude"] as! String
Longitude = value["Longitude"] as! String
Extrainformatie = value["Extrainformatie"] as! String
Staater = value["Staater"] as! String
Staaternietmeer = value["Staaternietmeer"] as! String
TijdControle = value["TijdControle"] as! String
TijdControleniet = value["TijdControleniet"] as! String
}
}
func save() {
ref.setValue(toDictionary())
}
func toDictionary() -> [String : Any]
{
return [
"TypeControle" : TypeControle,
"Stad" : Stad,
"Tijd" : Tijd,
"Latitude" : Latitude,
"Longitude" : Longitude,
"Extrainformatie" : Extrainformatie,
"Staater" : Staater,
"Staaternietmeer" : Staaternietmeer,
"TijdControle" : TijdControle,
"TijdControleniet" : TijdControleniet
]
}
}
Data for the TableViewCell:
class ControleTableViewCell: UITableViewCell {
#IBOutlet weak var storyControle: UILabel!
#IBOutlet weak var storyTijd: UILabel!
var post: Post! {
didSet {
storyControle.text = "\(post.Staaternietmeer)"
storyTijd.text = "\(post.TijdControleniet)"
storyControle.text = "\(post.Staater)"
storyTijd.text = "\(post.TijdControle)"
}
}
How my update button looks like:
#IBAction func Update(_ sender: Any) {
let alertController1 = UIAlertController(title: "Update melden" , message: "De rollerbank", preferredStyle: .alert)
// Create the actions
let RollerbankAction1 = UIAlertAction(title: "Staat er nog steeds", style: UIAlertActionStyle.default) {
UIAlertAction in
NSLog("Ja Pressed")
self.newStory.Staater = self.Staater
self.newStory.TijdControle = self.TijdControle
self.newStory.save()
}
let cancelAction1 = UIAlertAction(title: "Staat er niet meer", style: UIAlertActionStyle.cancel) {
UIAlertAction in
NSLog("Cancel Pressed")
let date = Date()
let calendar = Calendar.current
let hour = calendar.component(.hour, from: date)
let minutes = calendar.component(.minute, from: date)
let Tijd = "\(hour) : \(minutes)"
self.newStory.Staaternietmeer = self.Staaternietmeer
self.newStory.TijdControleniet = Tijd
self.newStory.save()
}
alertController1.addAction(RollerbankAction1)
alertController1.addAction(cancelAction1)
self.present(alertController1, animated: true, completion: nil)
}
This is the Structure that i use. If i run all this code, the new data will go in a other childbyAutoID and thats not what i want. It just needs to update/setvalue in the cleare space named "Staaternietmeer" and "TijdControleniet". Can anybody help me with that?
You would then need to store the Push ID somewhere so that you can reuse it later.
To generate a unique Push ID you would use :
Database.database().reference().childByAutoId()
And to store it somewhere :
let postKey = Database.database().reference().childByAutoId().key
And then, say you need a method to share a post for example, and want to add this post to multiple nodes, that's how it may look like :
func sharePost(_ postContent: String, completion: #escaping (Bool) -> ()) {
guard let currentUserId = Auth.auth().currentUser?.uid else {
completion(false)
return
}
let postKey = Database.database().reference().childByAutoId().key
let postData: [String: Any] = [ "content": "blabla",
"author": currentUserId ]
let childUpdates: [String: Any] = ["users/\(currentUserId)/posts/\(postKey)": true,
"posts/\(postKey)": postData ]
Database.database().reference().updateChildValues(childUpdates, withCompletionBlock: { (error, ref) in
guard error == nil else {
completion(false)
return
}
completion(true)
})
}
Now to access the unique Push ID later on, you would use :
Database.database().reference().observe(.childAdded, with: { (snapshot) in
// Here you get the Push ID back :
let postKey = snapshot.key
// And others things that you need :
guard let author = snapshot.childSnapshot(forPath: "author").value as? String else { return }
guard let content = snapshot.childSnapshot(forPath: "content").value as? String else { return }
// Here you store your object (Post for example) in an array, and as you can see you initialize your object using the data you got from the snapshot, including the Push ID (`postKey`) :
posts.append(Post(id: postKey, content: content, author: author))
})