SwiftUI: compiler takes initialized value from ViewModel rather than the value that has been fetched - swift

I'm writing a program where I reference a database where authenticated users each have a document whose ID corresponds to their User ID. Given the user's ID, I am trying to determine their name; I have managed to read all of the user's data and it is in my data model of class Users:
class Users {
var id: String
var name: String
var surname: String // ...
}
In my ViewModel, I have
#Published var specificUser = User(id: "", name: "", surname: "", email: "", profficiency: 0, lists: [[]])
which is an initialized user.
In that same ViewModel, I have a function that fetches the User Data from the database, which appears to work. It should then store the new user data in the specificUserData variable.
func getData() {
let db = Firestore.firestore()
guard let uid = auth.currentUser?.uid else { return }
db.collection("Users").getDocuments { result, error in
if error == nil {
print("Current User's ID found: \(uid)")
if let result = result {
// iterate through documents until correct ID is found
for d in result.documents {
if d.documentID == uid {
print("Document ID found: \(d.documentID)")
self.specificUser = User(
id: d.documentID,
name: d["name"] as? String ?? "",
// ...
)
print(self.specificUser)
print(self.specificUser.name) // This works; my compiler spits out the correct name from the database, so clearly the specificUser variable has been changed.
}
}
}
} else {
// Handle Error
print("Error while fetching user's specific data")
}
}
}
Here's how I initialized the getData() function:
init() {
model.getData()
print("Data Retrieval Complete")
print("User's Name: \(model.specificUser.name)")
}
I am trying to reference my ViewModel like this:
#ObservedObject var model = ViewModel()
Now here's the problem: when I try to reference the User's name from the view model in my struct with
model.specificUser.name
It gives me the default name, even though I have initialized the getData() function already. Checking my compiler log and adding a bunch of print statements, it appears that the initialization is in fact working, but it is printing data retrieval complete before it is printing the albeit correct name.
Any thoughts? It seems that the initializer function is taking the initialized value from my ViewModel rather than the correct value it should be computing.

try this
func getData(_ completion: #escaping (Bool, User?) -> ()) {
let db = Firestore.firestore()
guard let uid = auth.currentUser?.uid else { return }
db.collection("Users").getDocuments { result, error in
if error == nil {
print("Current User's ID found: \(uid)")
if let result = result {
// iterate through documents until correct ID is found
for d in result.documents {
if d.documentID == uid {
print("Document ID found: \(d.documentID)")
let user = User(
id: d.documentID,
name: d["name"] as? String ?? "",
// ...
)
completion(true, user)
print(self.specificUser)
print(self.specificUser.name) // This works; my compiler spits out the correct name from the database, so clearly the specificUser variable has been changed.
}
}
}
} else {
// Handle Error
completion(false, nil)
print("Error while fetching user's specific data")
}
}
}
init() {
model.getData() { res, user in
if res {
self.specificUser = user!
}
print("Data Retrieval Complete")
print("User's Name: \(model.specificUser.name)")
}
}

Related

How to make ForEach loop for a Codable Swift Struct's Dictionary (based on Firestore map)

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)
}

Swift retrieve user info from firebase firebase firestore

I want to retrieve the current logged in user Information (name and email) that was stored in the firestore in the registration function, the email and name should be displayed in textfield.
I can retrieve the email successfully because I’m using the Auth.auth().currentUser and not interacting with the firesotre while the name is not working for me.
what I’m suspecting is that the path I’m using for reaching the name field in firesotre is incorrect.
var id = ""
var email = ""
override func viewDidLoad() {
super.viewDidLoad()
userLoggedIn()
self.txtEmail.text = email
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
getName { (name) in
if let name = name {
self.txtUserName.text = name
print("great success")
}
}
}
func getName(completion: #escaping (_ name: String?) -> Void) {
guard let uid = Auth.auth().currentUser?.uid else { // safely unwrap the uid; avoid force unwrapping with !
completion(nil) // user is not logged in; return nil
return
}
print (uid)
Firestore.firestore().collection("users").document(uid).getDocument { (docSnapshot, error) in
if let doc = docSnapshot {
if let name = doc.get("name") as? String {
completion(name) // success; return name
} else {
print("error getting field")
completion(nil) // error getting field; return nil
}
} else {
if let error = error {
print(error)
}
completion(nil) // error getting document; return nil
}
}
}
func userLoggedIn() {
if Auth.auth().currentUser != nil {
id = Auth.auth().currentUser!.uid
//email = Auth.auth().currentUser!.email
} else {
print("user is not logged in")
//User Not logged in
}
if Auth.auth().currentUser != nil {
email = Auth.auth().currentUser!.email!
} else {
print("user is not logged in")
//User Not logged in
}
}
When I run this code the email is displayed and for the name "error getting field" gets printed so what I think is that the name of the document for user is not the same as the uid therefore the path I’m using is incorrect, the document name must be autogenerated.
So is the solution for me to change the code of the registration function?
can the user document be given a name (the userID) when I create the user document, instead of it being auto generarte it, if that’s even the case.
Here is the registration code for adding documents to firestore:
let database = Firestore.firestore()
database.collection("users").addDocument(data: [ "name" :name, "email" : email ]) { (error) in
if error != nil {
//
}
an here is a snapshot of my firestore users collection
When creating a user;
Auth.auth().createUser(withEmail: email, password: password) { authResult, error in
  // ...
}
At first you can only save email and password. (For now, that's how I know.)
But after you create the user, you can update the user's name.
let changeRequest = Auth.auth().currentUser?.createProfileChangeRequest()
changeRequest?.displayName = displayName
changeRequest?.commitChanges { error in
  // ...
}
Use userUID when saving user information in Firestore.
If you drop the document into firebase, it will create it automatically. But if you save the user uid, it will be easy to access and edit.
func userSave() {
let userUID = Auth.auth().currentUser?.uid
let data = ["name": "ABCD", "email": "abcd#abcd.com"]
Firestore.firestore().collection("users").document(userUID!).setData(data) { error in
if error != nil {
// ERROR
}
else {
// SUCCESSFUL
}
}
}
If you are saving user information in Firestore, you can retrieve information very easily.
func fetchUser() {
let userUID = Auth.auth().currentUser?.uid
Firestore.firestore().collection("users").document(userUID!).getDocument { snapshot, error in
if error != nil {
// ERROR
}
else {
let userName = snapshot?.get("name")
}
}
}
For more detailed and precise information: Cloud Firestore Documentation
If you see missing or incorrect information, please warn. I will fix it.
There's a distinction between a Firebase User property displayName and then other data you're stored in the Firestore database.
I think from your question you're storing other user data (a name in this case) in the Firestore database. The problem is where you're storing it is not the same as where you're reading it from.
According to your code here's where it's stored
database.collection("users").addDocument(data: [ "name" :name,
which looks like this
firestore
users
a 'randomly' generated documentID <- not the users uid
name: ABCD
email: abcd#email.com
and that's because addDocument creates a documentID for you
Where you're trying to read it from is the actual users UID, not the auto-created documentID from above
Firestore.firestore().collection("users").document(userUID!)
which looks like this
firestore
users
the_actual_users_uid <- the users uid
name: ABCD
email: abcd#email.com
The fix it easy, store the data using the users uid to start with
database.collection("users").document(uid).setData(["name" :name,

Swift+Firestore - return from getDocument() function

I'm currently dealing with this problem. I have Firestore database. My goal is to fill friends array with User entities after being fetched. I call fetchFriends, which fetches currently logged user, that has friends array in it (each item is ID of friend). friends array is then looped and each ID of friend is fetched and new entity User is made. I want to map this friends array to friends Published variable. What I did there does not work and I'm not able to come up with some solution.
Firestore DB
User
- name: String
- friends: [String]
User model
struct User: Identifiable, Codable {
#DocumentID var id: String?
var name: String?
var email: String
var photoURL: URL?
var friends: [String]?
}
User ViewModel
#Published var friends = [User?]()
func fetchFriends(uid: String) {
let userRef = db.collection("users").document(uid)
userRef.addSnapshotListener { documentSnapshot, error in
do {
guard let user = try documentSnapshot?.data(as: User.self) else {
return
}
self.friends = user.friends!.compactMap({ friendUid in
self.fetchUserAndReturn(uid: friendUid) { friend in
return friend
}
})
}
catch {
print(error)
}
}
}
func fetchUserAndReturn(uid: String, callback:#escaping (User)->User) {
let friendRef = db.collection("users").document(uid)
friendRef.getDocument { document, error in
callback(try! document?.data(as: User.self) as! User)
}
}
One option is to use DispatchGroups to group up the reading of the users but really, the code in the question is not that far off.
There really is no need for compactMap as user id's are unique and so are documentId's within the same collection so there shouldn't be an issue where there are duplicate userUid's as friends.
Using the user object in the question, here's how to populate the friends array
func fetchFriends(uid: String) {
let userRef = db.collection("users").document(uid)
userRef.addSnapshotListener { documentSnapshot, error in
guard let user = try! documentSnapshot?.data(as: User.self) else { return }
user.friends!.forEach( { friendUid in
self.fetchUserAndReturn(uid: friendUid, completion: { returnedFriend in
self.friendsArray.append(returnedFriend)
})
})
}
}
func fetchUserAndReturn(uid: String, completion: #escaping ( MyUser ) -> Void ) {
let userDocument = self.db.collection("users").document(uid)
userDocument.getDocument(completion: { document, error in
guard let user = try! document?.data(as: User.self) else { return }
completion(user)
})
}
Note that I removed all the error checking for brevity so be sure to include checking for Firebase errors as well as nil objects.

Updating a codable user struct

I'm developing an app where the user can create up to 5 profiles when I came across a problem.
Problem:
Fatal error: Unexpectedly found nil while unwrapping an Optional value: file
Info:
The first edition of the app has a struct with these data points in it:
id
profileName
profileIcon
When the app opens it loads the user via func loadUser()
Now, during an update I've added a new data point in the user struct so it now looks like this:
id
profileName
profileIcon
profileSummary
Now when the func loadUser() is called it fails with this statement:
Fatal error: Unexpectedly found nil while unwrapping an Optional value: file
How the system is built:
When the app is opened the first time it creates 5 empty profiles. The user can then "activate" and fill out these profiles and he/she likes.
I'm a little uncertain how to deal with this problem. How can I add new data points to my struct without causing the app to crash?
Source code:
struct User: Codable {
// Core user data
let id: Int
var profileName: String
var profileIcon: String
var profileSummary: String
}
class DataManager: NSObject {
/// Used to encode and save user to UserDefaults
func saveUser(_ user: User) {
if let encoded = try? JSONEncoder().encode(user) {
UserDefaults.standard.set(encoded, forKey: "userProfile_\(user.id)")
print("Saved user (ID: \(user.id)) successfully.")
}
}
/// Used to decode and load user from UserDefaults
func loadUser(_ userID: Int) -> User {
var user : User?
if let userData = UserDefaults.standard.data(forKey: "userProfile_\(userID)"),
let userFile = try? JSONDecoder().decode(User.self, from: userData) {
user = userFile
print("Loaded user (ID: \(user!.id)) successfully.")
}
return user!
}
/// Used to create x empty userprofiles ready to be used
func createEmptyProfiles() {
// May be changed, but remember to adjust UI
var profilesAllowed = 5
while profilesAllowed != 0 {
print("Attempting to create empty profile for user with ID \(profilesAllowed)")
let user = User(id: profilesAllowed, profileName: "", profileIcon: "", profileSummary: "Write a bit about your profile here..")
self.saveUser(user)
print("User with ID \(profilesAllowed) was created and saved successfully")
// Substract one
profilesAllowed -= 1
}
}
//MARK: - Delete profile
func deleteUser(user: User) -> Bool {
var userHasBeenDeleted = false
var userToDelete = user
// Reset all values
userToDelete.profileName = ""
userToDelete.profileIcon = ""
userToDelete.profileSummary = ""
// Save the resetted user
if let encoded = try? JSONEncoder().encode(userToDelete) {
UserDefaults.standard.set(encoded, forKey: "userProfile_\(user.id)")
print("User has now been deleted")
userHasBeenDeleted = true
}
return userHasBeenDeleted
}
}
Two options:
Make profileSummary optional
struct User: Codable {
// Core user data
let id: Int
var profileName: String
var profileIcon: String
var profileSummary: String?
}
If the key doesn't exist it will be ignored.
Implement init(from decoder) and decode profileSummary with decodeIfPresent assigning an empty string if it isn't.
Side note:
Never try? in a Codable context. Catch the error and handle it. Your loadUser method crashes reliably if an error occurs. A safe way is to make the method throw and hand over errors to the caller.
enum ProfileError : Error { case profileNotAvailable }
/// Used to decode and load user from UserDefaults
func loadUser(_ userID: Int) throws -> User {
guard let userData = UserDefaults.standard.data(forKey: "userProfile_\(userID)") else { throw ProfileError.profileNotAvailable }
return try JSONDecoder().decode(User.self, from: userData)
}

Vapor 3 - How to check for similar email before saving object

I would like to create a route to let users update their data (e.g. changing their email or their username). To make sure a user cannot use the same username as another user, I would like to check if a user with the same username already exists in the database.
I have already made the username unique in the migrations.
I have a user model that looks like this:
struct User: Content, SQLiteModel, Migration {
var id: Int?
var username: String
var name: String
var email: String
var password: String
var creationDate: Date?
// Permissions
var staff: Bool = false
var superuser: Bool = false
init(username: String, name: String, email: String, password: String) {
self.username = username
self.name = name
self.email = email
self.password = password
self.creationDate = Date()
}
}
This is the piece of code where I want to use it:
func create(_ req: Request) throws -> EventLoopFuture<User> {
return try req.content.decode(UserCreationRequest.self).flatMap { userRequest in
// Check if `userRequest.email` already exists
// If if does -> throw Abort(.badRequest, reason: "Email already in use")
// Else -> Go on with creation
let digest = try req.make(BCryptDigest.self)
let hashedPassword = try digest.hash(userRequest.password)
let persistedUser = User(name: userRequest.name, email: userRequest.email, password: hashedPassword)
return persistedUser.save(on: req)
}
}
I could do it like this (see next snippet) but it seems a strange option as it requires a lot of nesting when more checks for e.g. uniqueness would have to be performed (for instance in the case of updating a user).
func create(_ req: Request) throws -> EventLoopFuture<User> {
return try req.content.decode(UserCreationRequest.self).flatMap { userRequest in
let userID = userRequest.email
return User.query(on: req).filter(\.userID == userID).first().flatMap { existingUser in
guard existingUser == nil else {
throw Abort(.badRequest, reason: "A user with this email already exists")
}
let digest = try req.make(BCryptDigest.self)
let hashedPassword = try digest.hash(userRequest.password)
let persistedUser = User(name: userRequest.name, email: userRequest.email, password: hashedPassword)
return persistedUser.save(on: req)
}
}
}
As one of the answers suggested I've tried to add Error middleware (see next snippet) but this does not correctly catch the error (maybe I am doing something wrong in the code - just started with Vapor).
import Vapor
import FluentSQLite
enum InternalError: Error {
case emailDuplicate
}
struct EmailDuplicateErrorMiddleware: Middleware {
func respond(to request: Request, chainingTo next: Responder) throws -> EventLoopFuture<Response> {
let response: Future<Response>
do {
response = try next.respond(to: request)
} catch is SQLiteError {
response = request.eventLoop.newFailedFuture(error: InternalError.emailDuplicate)
}
return response.catchFlatMap { error in
if let response = error as? ResponseEncodable {
do {
return try response.encode(for: request)
} catch {
return request.eventLoop.newFailedFuture(error: InternalError.emailDuplicate)
}
} else {
return request.eventLoop.newFailedFuture(error: error)
}
}
}
}
The quick way of doing it is to do something like User.query(on: req).filter(\.email == email).count() and check that equals 0 before attempting the save.
However, whilst this will work fine for almost everyone, you still risk edge cases where two users try to register with the same username at the exact same time - the only way to handle this is to catch the save failure, check if it was because the unique constraint on the email and return the error to the user. However the chances of you actually hitting that are pretty rare, even for big apps.
I would make the field unique in the model using a Migration such as:
extension User: Migration {
static func prepare(on connection: SQLiteConnection) -> Future<Void> {
return Database.create(self, on: connection) { builder in
try addProperties(to: builder)
builder.unique(on: \.email)
}
}
}
If you use a default String as the field type for email, then you will need to reduce it as this creates a field VARCHAR(255) which is too big for a UNIQUE key. I would then use a bit of custom Middleware to trap the error that arises when a second attempt to save a record is made using the same email.
struct DupEmailErrorMiddleware: Middleware
{
func respond(to request: Request, chainingTo next: Responder) throws -> EventLoopFuture<Response>
{
let response: Future<Response>
do {
response = try next.respond(to: request)
} catch is MySQLError {
// needs a bit more sophistication to check the specific error
response = request.eventLoop.newFailedFuture(error: InternalError.dupEmail)
}
return response.catchFlatMap
{
error in
if let response = error as? ResponseEncodable
{
do
{
return try response.encode(for: request)
}
catch
{
return request.eventLoop.newFailedFuture(error: InternalError.dupEmail)
}
} else
{
return request.eventLoop.newFailedFuture(error: error )
}
}
}
}
EDIT:
Your custom error needs to be something like:
enum InternalError: Debuggable, ResponseEncodable
{
func encode(for request: Request) throws -> EventLoopFuture<Response>
{
let response = request.response()
let eventController = EventController()
//TODO make this return to correct view
eventController.message = reason
return try eventController.index(request).map
{
html in
try response.content.encode(html)
return response
}
}
case dupEmail
var identifier:String
{
switch self
{
case .dupEmail: return "dupEmail"
}
}
var reason:String
{
switch self
{
case .dupEmail: return "Email address already used"
}
}
}
In the code above, the actual error is displayed to the user by setting a value in the controller, which is then picked up in the view and an alert displayed. This method allows a general-purpose error handler to take care of displaying the error messages. However, in your case, it might be that you could just create the response in the catchFlatMap.