I have a question where and how to properly save users who are logged into a rooms, now I save them inside the collection "rooms" but I save the user as a reference
func addUserToRoom(room: String, id: String) {
COLLETCTION_ROOMS.document(room).updateData(["members": FieldValue.arrayUnion([COLLETCTION_USERS.document(id)])])
}
But when I create a rooms, I need to pass in an array of users
func fetchRooms() {
Firestore.firestore()
.collection("rooms")
.addSnapshotListener { snapshot, _ in
guard let rooms = snapshot?.documents else { return }
self.rooms = rooms.map({ (queryDocumentSnapshot) -> VoiceRoom in
let data = queryDocumentSnapshot.data()
let query = queryDocumentSnapshot.get("members") as? [DocumentReference]
// ======== I'm making a request here, but I don't understand how to save users next
query.map { result in
result.map { user in
let user = user.getDocument { userSnapshot, _ in
let data = userSnapshot?.data()
guard let user = data else { return }
let id = user["id"] as? String ?? ""
let first_name = user["first_name"] as? String ?? ""
let last_name = user["last_name"] as? String ?? ""
let profile = Profile(id: id,
first_name: first_name,
last_name: last_name)
}
}
}
let id = data["id"] as? String ?? ""
let title = data["title"] as? String ?? ""
let description = data["description"] as? String ?? ""
let members = [Profile(id: "1", first_name: "Test1", last_name: "Test1")]
return VoiceRoom(id: id,
title: title,
description: description,
members: members
})
}
}
This is how my room and profile model looks like
struct VoiceRoom: Identifiable, Decodable {
var id: String
var title: String
var description: String
var members: [Profile]?
}
struct Profile: Identifiable, Decodable {
var id: String
var first_name: String?
var last_name: String?
}
Maybe someone can tell me if I am not saving users correctly and I need to do it in a separate collection, so far I have not found a solution, I would be grateful for any advice.
I feel like this answer is going to blow your mind:
struct VoiceRoom: Identifiable, Codable {
var id: String
var title: String
var description: String
var members: [Profile]?
}
struct Profile: Identifiable, Codable {
var id: String
var first_name: String?
var last_name: String?
}
final class RoomRepository: ObservableObject {
#Published var rooms: [VoiceRoom] = []
private let db = Firestore.firestore()
private var listener: ListenerRegistration?
func addUserToRoom(room: VoiceRoom, user: Profile) {
let docRef = COLLECTION_ROOMS.document(room.id)
let userData = try! Firestore.Encoder().encode(user)
docRef.updateData(["members" : FieldValue.arrayUnion([userData])])
}
func fetchRooms() {
listener = db.collection("rooms").addSnapshotListener { snapshot, _ in
guard let roomDocuments = snapshot?.documents else { return }
self.rooms = roomDocuments.compactMap { try? $0.data(as: VoiceRoom.self) }
}
}
}
And that's it. This is all it takes to correctly store members of a room and simply decode a stored room into an instance of VoiceRoom. All of it is pretty self explanatory but if you have any questions feel free to ask it in the comments.
P.S. I changed COLLETCTION_ROOMS to COLLECTION_ROOMS
Edit:
I decided to elaborate on my changes anyway so that people who just started coding can understand how I reduced the code to just a few lines (skip this if you understood my code).
When your custom data instances conform to the Codable protocol it allows those instances to be automatically transformed into data the database can store (known as Encoding) AND allows them to converted back into the concrete types in your code when you retrieve them from the database (known as Decoding). And the magic of it is all you need to do is add : Codable to your structs and classes to get all this functionality for free!
Now, what about the functions? The addUserToRoom(room:user:) function takes in a VoiceRoom instead of a String because it's generally easier for the caller to just pass in room instead of room.id. This extra step can be done by the function itself and saves a little bit of clutter in your Views while also making it easier.
Finally the fetchRooms() function attaches a listener to the "rooms" collection and fires a block of code any time the collection changes in any way. In the block of code, I check if the document snapshot actually contains documents by using guard let roomDocuments = snapshot?.documents else { return }. After I know I have the documents all that's left to do is convert those documents back into VoiceRoom instances which can easily be done because VoiceRoom conforms to Codable (Remember how I said: "AND allows them to converted back into the concrete types in your code when you retrieve them from the database"). I do this by mapping the array of [QueryDocumentSnapshot] i.e. roomDocuments, into concrete VoiceRoom instances. I use a compact map because if any of the documents fail to decode it won't be contained in self.rooms.
So
try? $0.data(as: VoiceRoom.self)
tries to take every document (represented by $0) and represent its "data" "as" "VoiceRoom" instances.
And that's all it takes!
Related
I am trying to do a ForEach loop that lists all of the social medias a user might have. This would be on a scrollable list, the equivalent of music streaming apps have a list of all the songs you save in your library. The user's social medias list is in reality a dictionary.
My MainViewModel class implements all of the Firebase functionality. See below.
class MainViewModel: ObservableObject {
#Published var errorMessage = ""
#Published var user: User?
init() {
DispatchQueue.main.async {
self.isUserCurrentlyLoggedOut = FirebaseManager.shared.auth.currentUser?.uid == nil
}
readCodableUserWithMap()
}
#Published var isUserCurrentlyLoggedOut = false
func handleSignOut() {
isUserCurrentlyLoggedOut.toggle()
try? FirebaseManager.shared.auth.signOut()
}
func readCodableUserWithMap() {
guard let uid = FirebaseManager.shared.auth.currentUser?.uid else {
self.errorMessage = "Could not find firebase uid"
print("FAILED TO FIND UID")
return
}
let userID = uid
let docRef = FirebaseManager.shared.firestore.collection("users").document(userID)
docRef.getDocument { (document, error) in
if let err = error {
print(err.localizedDescription)
return
}
if let doc = document {
let user = try! doc.data(as: User.self)
if let mappedField = user.socials {
mappedField.forEach { print($0.key, $0.value) }
}
}
}
}
}
readCodableUserWithMap() is supposed to initialize my codable struct, which represents a user. See below.
struct User: Identifiable, Codable {
#DocumentID var id: String?
var socials: [String: String]?
var uid, email, name, bio, profileImageUrl: String?
var numSocials, followers, following: Int?
}
QUESTION AT HAND: In my Dashboard View, I am trying to have a list of all the social medias a user can have. I can't manage to make a ForEach loop for my user.
I do:
ForEach(vm.user?.socials.sorted(by: >), id: \.key) { key, value in
linkDisplay(social: key, handler: value)
.listRowSeparator(.hidden)
}.onDelete(perform: delete)
This gives me the following errors:
Value of optional type '[Dictionary<String, String>.Element]?' (aka 'Optional<Array<(key: String, value: String)>>') must be unwrapped to a value of type '[Dictionary<String, String>.Element]' (aka 'Array<(key: String, value: String)>')
Value of optional type '[String : String]?' must be unwrapped to refer to member 'sorted' of wrapped base type '[String : String]'
I have tried coalescing using ?? but that doesn't work, although I assume I am doing something wrong. Force-unwrapping is something I tried but it made the app crash when it was an empty dictionary.
Just in case, here is the database hierarchy: firebase hierarchy
TLDR: HOW can I make a ForEach loop to list all of my user's medias?
You have both vm.user and socials as optionals.
The ForEach loop requires non-optionals, so
you could try the following approach to unwrap those for the ForEach loop.
if let user = vm.user, let socials = user.socials {
ForEach(socials.sorted(by: > ), id: \.key) { key, value in
linkDisplay(social: key, handler: value)
.listRowSeparator(.hidden)
}.onDelete(perform: delete)
}
I've simplified my code down so it's easier to see what i'm trying to do, I am building a feed of ratings that are sourced from firebase documents of whomever I am "following"
All that I want to do is whenever firebase says there is a new update to one of my reviews, change that Identifiable, as apposed to having to re-load every review from every person i'm following.
Here's my view:
import SwiftUI
import Firebase
struct Rating: Identifiable {
var id: String
var review: String
var likes: Int
}
struct Home: View {
#ObservedObject var feedViewModel = FeedViewModel()
var body: some View {
VStack{
ForEach(self.feedViewModel.allRatings, id: \.id){ rating in
Text(rating.review)
Text("\(rating.likes)")
}
}
}
}
Here are my functions for FeedViewModel:
class FeedViewModel: ObservableObject {
#Published var following = ["qedXpEcaRLhIa6zWjfJC", "1nDyDT4bIa7LBEaYGjHG", "4c9ZSPTQm2zZqztNlVUp", "YlvnziMdW8VfibEyCUws"]
#Published var allRatings = [Rating]()
init() {
for doc in following {
// For each person (aka doc) I am following we need to load all of it's ratings and connect the listeners
getRatings(doc: doc)
initializeListener(doc: doc)
}
}
func getRatings(doc: String) {
db.collection("ratings").document(doc).collection("public").getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
// We have to use += because we are loading lots of different documents into the allRatings array, allRatings turns into our feed of reviews.
self.allRatings += querySnapshot!.documents.map { queryDocumentSnapshot -> Rating in
let data = queryDocumentSnapshot.data()
let id = ("\(doc)-\(queryDocumentSnapshot.documentID)")
let review = data["review"] as? String ?? ""
let likes = data["likes"] as? Int ?? 0
return Rating(id: id, review: review, likes: likes)
}
}
}
func initializeListener(doc: String){
db.collection("ratings").document(doc).collection("public")
.addSnapshotListener { (querySnapshot, error) in
guard let snapshot = querySnapshot else {
print("Error listening for channel updates")
return
}
snapshot.documentChanges.forEach { change in
for document in snapshot.documents{
let data = document.data()
let id = ("\(doc)-\(document.documentID)")
let review = data["review"] as? String ?? ""
let likes = data["likes"] as? Int ?? 0
// I thought swiftui Identifiables would be smart enough to not add another identifiable if there is already an existing one with the same id, but I was wrong, this just duplicates the rating
// self.allRatings.append(Rating(id: id, review: review, likes: likes))
// This is my current solution, to overwrite the Rating in the allRatings array, however I can not figure out how to get the index of the changed rating
// "0" should be replaced with whatever Rating index is being changed
self.allRatings[0] = Rating(id: id, review: review, likes: likes)
}
}
}
}
}
All I want is to make the "likes" of each rating live and update whenever someone likes a rating. It seems simple but I am relatively new to swiftui so I might be completely off on how I am doing this. Any help is greatly appreciated!
After a week of trying to figure this out I finally got it, hopefully this helps somebody, if you want to overwrite an identifiable in your array find the index by using your id. In my case my ID is the firebaseCollectionID + firebaseDocumentID, so this makes it always 100% unique AND it allows me to reference it from a snapshot listener...
let index = self.allRatings.firstIndex { (Rating) -> Bool in
Rating.id == id
}
Then overwrite it by doing:
self.allRatings[index!] = Rating(id: id, review: review, likes: likes)
I have this code to get fields "spty" with autoID from collection "specialities":
struct Spty: Identifiable{
var id: String = UUID().uuidString
var spty: String
}
class SptyViewModel: NSObject, ObservableObject{
#Published var specialities = [Spty]()
func fetchData(){
let db = Firestore.firestore()
db.collection("specialities").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {return }
self.specialities = documents.map { (queryDocumentSnapshot) -> Spty in
let data = queryDocumentSnapshot.data()
let spty = data["spty"] as? String ?? ""
return Spty(spty: spty)
}
}
}
}
They are displayed by using ForEach:
#StateObject var sptyModel = SptyViewModel()
ForEach(sptyModel.specialities){ spty in
NavigationLink(destination: More()) {
Text(spty.spty).foregroundColor(.black)
As in the code, each Text(spty.spty) is a NavigationLink, so what I want is to display different information according to the Text that I pressed.
Im currently working on this getDocuments:
struct Testtt: Identifiable{
var id: String = UUID().uuidString
var name: String
}
class TesteViewModel: NSObject,ObservableObject{
#StateObject var LocationModel = LocationViewModel()
let db = Firestore.firestore()
#Published var testsss = [Testtt]()
#Published var sptyModel = SptyViewModel()
#Published var SptyNameee = [Spty]()
func testeApp(){
db.collection("Test").whereField("Los Angeles", isEqualTo: true).getDocuments { (querySnapshot, err) in
guard let documents = querySnapshot?.documents else {return }
self.testsss = documents.map { (queryDocumentSnapshot) -> Testtt in
let data = queryDocumentSnapshot.data()
let name = data["Name"] as? String ?? ""
return Testtt(name: name)
}
}
}
}
ignore .whereField
What I've been thinking is that to answer this question I need to relate collection() in testeApp() with spty.spty , but didn't managed to do it.
Any ideas?
EDIT:
[![2
][4]][4]
To clarify my question I added screenshots on how Im currently managing the data:
I get 3 NavLinks in Home View accordion to collection "specialities" and the code given above: Cardiologista, Cirurgião Geral and Oftalmoloista
Then, if I press any of the NavLinks, I got to More View
There, what I want is: if the user presses Cardiologista, the name "Mateus Neves" will appear for him. But, I he presses Oftalmologista, the name "Vitor Souza" will appear.
I am going to attempt address a comment in the question first, which may lead to the answer.
If you click “Top Paid Apps”, X apps will be displayed. If you click
on “Top Free Apps”, Y apps will be displayed
Here's a structure that supports that UI
allApps (a collection)
app_0 (a document)
app_type = "Free"
app_1
app_type = "Paid"
As you can see, if a user clicks on Free apps, a query is run against Firestore for all documents where app_type isEqual(to: "Free")
So then let's expand on that. Lets take specialty types and store them in a collection - this would be the list presented to the user that can select from
specialties
type_0
name: "Cardiologist"
type_1
name: "General"
type_2
name: "Anesthesiologist"
then the doctors would be stored in a separate collection
doctors
doctor_0
name: "Dr. Smith"
type: "type_0"
doctor_1
name: "Dr. Jones"
type: "type_1"
doctor_2
name: "Dr. Jenkins"
type: "type_2"
So when the user clicks on Cardiologist in the list, you know that's type_0 and then query the doctors node for all documents where type isEqual(to: "type_0") which returns Dr. Smith.
Note
Make sure you map the documents into an object in code that will hold both the type (the documentId) as the name so you can use that as a tableView datasouce.
class MyType {
var type = "" //the firestore documentId
var name = ""
}
var myTableViewDataSourceArrayOfTypes = [MyType]() //the tableView datasource
I am using #DocumentId in my data struct:
struct UserProfile : Codable, Identifiable {
#DocumentID var id: String? = UUID().uuidString
}
When I try to access the id I get a random string of letters (04F9C67E-C4A5-4870-9C22-F52C7F543AA5) Instead of the name of the document in firestore (GeG23o4GNJt5CrKEf3RS)
To access the documentId I am using the following code in an ObservableObject:
class Authenticator: ObservableObject {
#Published var currentUser: UserProfile = UserProfile()
#Published var user: String = ""
func getCurrentUser(viewModel: UsersViewModel) -> String {
guard let userID = Auth.auth().currentUser?.uid else {
return ""
}
viewModel.users.forEach { i in
if (i.userId == userID) {
currentUser = i
}
}
print("userId \(currentUser.id ?? "no Id")")
print("name \(currentUser.name)")
return userID
}
Where currentUser is a UserProfile struct. currentUser.name returns the proper value. What am I doing wrong?
currentUser is a member of an array populated using the following method:
func fetchData() {
db.collection("Users").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.users = documents.compactMap { (queryDocumentSnapshot) -> UserProfile? in
return try? queryDocumentSnapshot.data(as: UserProfile.self)
}
}
}
The id of the UserProfile struct and the id of your data in Firestore are separate. That's why you're seeing the random string of letters.
I would change your struct similar to the below and you can just ignore the regular "id" variable when you initialize and refer to the documentID instead.
struct UserProfile : Codable, Identifiable {
var id = UUID() // ID for the struct
var documentID: String // ID for the post in Database
}
The problem is that you're overwriting the fetched DocumentID with a newly generated UUID string. You should keep it uninitialized, there is no need to set an ID manually. Even if there is, then you should set it only if the object is created from code but not if it gets decoded from Firestore
found my specific problem, I defined CodingKeys enum and when you do that, you have to make sure that you have a mapping for every attribute.
make sure you put all of your model attributes in CodingKeys enum or don't use custom CodingKeys at all
EDIT: taking the above back...it's still buggy...maybe decodes fine but not encodes and uploading
my workaround for now is to not use .adddocument but instead .setdataenter image description here
I want to save the array patientList in UserDefaults. Patient is an custom class so I need to transfer it into Data object, but this doesn't work on Swift 5 like it did before.
func addFirstPatient(){
let newPatient = Patient(name: nameField.text!, number: numberField.text!, resultArray: resultArray, diagnoseArray: diagnoseArray)
let patientList: [Patient] = [newPatient]
let encodeData: Data = NSKeyedArchiver.archivedData(withRootObject: patientList)
UserDefaults.standard.set(encodeData, forKey: "patientList")
UserDefaults.standard.synchronize()
}
struct Patient {
var diagnoseArray: [Diagnose]
var resultArray: [Diagnose]
var name: String
var number: String
init(name: String, number: String, resultArray: [Diagnose], diagnoseArray: [Diagnose]) {
self.diagnoseArray = diagnoseArray
self.name = name
self.number = number
self.resultArray = resultArray
}
}
struct Diagnose{
var name: String
var treatments: [Treatment]
var isPositiv = false
var isExtended = false
init(name: String, treatments: [Treatment]) {
self.name = name
self.treatments = treatments
}
}
struct Treatment {
var name: String
var wasMade = false
init(name: String) {
self.name = name
}
}
This is what the function looks like.
The problem is in the line where I initialize encodeData.
let encodeData: Data = try! NSKeyedArchiver.archivedData(withRootObject: patientList, requiringSecureCoding: false)
This is what Swift suggests but when I try it like this it always crashes and I don't get the error
You cannot use NSKeyedArchiver with structs at all. The objects must be subclasses of NSObject which adopt NSCoding and implement the required methods.
As suggested in the comments Codable is the better choice for example
struct Patient : Codable {
var name: String
var number: String
var resultArray: [Diagnose]
var diagnoseArray: [Diagnose]
}
struct Diagnose : Codable {
var name: String
var treatments: [Treatment]
var isPositiv : Bool
var isExtended : Bool
}
struct Treatment : Codable {
var name: String
var wasMade : Bool
}
let newPatient = Patient(name: "John Doe",
number: "123",
resultArray: [Diagnose(name: "Result", treatments: [Treatment(name: "Treat1", wasMade: false)], isPositiv: false, isExtended: false)],
diagnoseArray: [Diagnose(name: "Diagnose", treatments: [Treatment(name: "Treat2", wasMade: false)], isPositiv: false, isExtended: false)])
let patientList: [Patient] = [newPatient]
do {
let encodeData = try JSONEncoder().encode(patientList)
UserDefaults.standard.set(encodeData, forKey: "patientList")
// synchronize is not needed
} catch { print(error) }
If you want to provide default values for the Bool values you have to write an initializer.
Vadian's answer is correct, you cannot use NSKeyedArchiver with structs. Having all your objects conform to Codable is the best way to reproduce the behavior you are looking for. I do what Vadian does, but I you can also use protocol extensions to make this safer.
import UIKit
struct Patient: Codable {
var name: String
var number: String
var resultArray: [Diagnose]
var diagnoseArray: [Diagnose]
}
struct Diagnose: Codable {
var name: String
var treatments: [Treatment]
var isPositiv : Bool
var isExtended : Bool
}
struct Treatment: Codable {
var name: String
var wasMade : Bool
}
let newPatient = Patient(name: "John Doe",
number: "123",
resultArray: [Diagnose(name: "Result", treatments: [Treatment(name: "Treat1", wasMade: false)], isPositiv: false, isExtended: false)],
diagnoseArray: [Diagnose(name: "Diagnose", treatments: [Treatment(name: "Treat2", wasMade: false)], isPositiv: false, isExtended: false)])
let patientList: [Patient] = [newPatient]
Introduce a protocol to manage the encoding and saving of objects.
This does not have to inherit from Codable but it does for this example for simplicity.
/// Objects conforming to `CanSaveToDisk` have a save method and provide keys for saving individual objects or a list of objects.
protocol CanSaveToDisk: Codable {
/// Provide default logic for encoding this value.
static var defaultEncoder: JSONEncoder { get }
/// This key is used to save the individual object to disk. This works best by using a unique identifier.
var storageKeyForObject: String { get }
/// This key is used to save a list of these objects to disk. Any array of items conforming to `CanSaveToDisk` has the option to save as well.
static var storageKeyForListofObjects: String { get }
/// Persists the object to disk.
///
/// - Throws: useful to throw an error from an encoder or a custom error if you use stage different from user defaults like the keychain
func save() throws
}
Using protocol extensions we add an option to save an array of these objects.
extension Array where Element: CanSaveToDisk {
func dataValue() throws -> Data {
return try Element.defaultEncoder.encode(self)
}
func save() throws {
let storage = UserDefaults.standard
storage.set(try dataValue(), forKey: Element.storageKeyForListofObjects)
}
}
We extend our patient object so it can know what to do when saving.
I use "storage" so that this could be swapped with NSKeychain. If you are saving sensitive data (like patient information) you should be using the keychain instead of UserDefaults. Also, make sure you comply with security and privacy best practices for health data in whatever market you're offering your app. Laws can be a very different experience between countries. UserDefaults might not be safe enough storage.
There are lots of great keychain wrappers to make things easier. UserDefaults simply sets data using a key. The Keychain does the same. A wrapper like https://github.com/evgenyneu/keychain-swift will behave similar to how I use UserDefaults below. I have commented out what the equivalent use would look like for completeness.
extension Patient: CanSaveToDisk {
static var defaultEncoder: JSONEncoder {
let encoder = JSONEncoder()
// add additional customization here
// like dates or data handling
return encoder
}
var storageKeyForObject: String {
// "com.myapp.patient.123"
return "com.myapp.patient.\(number)"
}
static var storageKeyForListofObjects: String {
return "com.myapp.patientList"
}
func save() throws {
// you could also save to the keychain easily
//let keychain = KeychainSwift()
//keychain.set(dataObject, forKey: storageKeyForObject)
let data = try Patient.defaultEncoder.encode(self)
let storage = UserDefaults.standard
storage.setValue(data, forKey: storageKeyForObject)
}
}
Saving is simplified, check out the 2 examples below!
do {
// saving just one patient record
// this saves this patient to the storageKeyForObject
try patientList.first?.save()
// saving the entire list
try patientList.save()
} catch { print(error) }
struct Employee: Codable{
var name: String
}
var emp1 = Employee(name: "John")
let encoder = JSONEncoder()
do {
let data = try encoder.encode(emp1)
UserDefaults.standard.set(data, forKey: "employee")
UserDefaults.standard.synchronize()
} catch {
print("error")
}