I am trying to replace manual mapping when fetching a user by using Codable. In doing so, I am getting some errors. The errors I am getting are 'cannot find self in scope'.
Here is my fetchUser function:
import SwiftUI
import Firebase
import FirebaseFirestoreSwift
func fetchUser(documentId: String) {
let db = Firestore.firestore()
let docRef = db.collection("Users").document(documentId)
docRef.getDocument { document, error in
if let error = error as NSError? {
self.errorMessage = "Error getting document: \(error.localizedDescription)"
}
else {
if let document = document {
do {
self.user = try document.data(as: UserModel.self)
}
catch {
print(error)
}
}
}
}
}
Here is my UserModel:
import SwiftUI
import FirebaseFirestoreSwift
struct UserModel: Identifiable, Codable{
#DocumentID var id: String?
var username : String
var pic : String
var bio: String
var uid : String
var isVerified: Bool
var favouriteProducts: [FavourtieProducts]
}
What's going wrong here?
The following is how I did it before. This worked but when I added more variables to the UserModel I wanted to change how it is fetched to make the code better.
import SwiftUI
import Firebase
import FirebaseFirestoreSwift
let ref = Firestore.firestore()
func fetchUser(uid: String,completion: #escaping (UserModel) -> ()){
let db = Firestore.firestore()
#EnvironmentObject var sharedData: SharedDataModel
ref.collection("Users").document(uid).getDocument { (doc, err) in
guard let user = doc else{return}
let username = user.data()?["username"] as? String ?? "No Username"
let pic = user.data()?["imageurl"] as? String ?? "No image URL"
let bio = user.data()?["bio"] as? String ?? "No bio"
let uid = user.data()?["uid"] as? String ?? ""
let isVerified = user.data()?["isVerified"] as? Bool ?? false
DispatchQueue.main.async {
completion(UserModel(username: username, pic: pic, bio: bio, uid: uid, isVerified: isVerified))
}
}
}
How do I turn the first(new one) fetchUser function from a global function to a local function?
Related
I am trying to store a struct called 'UnlockingCharacters' in the users document on firebase. I have a struct called 'Character'. When a user taps "unlock" on a character, the 'Character' is added to 'UnlockingCharacters'. I need to store this on firebase in the users document but am struggling to do this.
I have managed to add a 'Character' to 'UnlockingCharacters' and display them in the users profile however it is not stored in firebase so when the app is closed, the 'Character' is no longer in 'UnlockingCharacters'
Here are my structs & classes:
struct Character: Identifiable, Codable {
#DocumentID var id: String?
var character_name: String
var character_type: String
var character_image: String
var character_details: String
var character_usersUnlocking: Int
var character_totalPoints: Int
var user: UserModel?
var didUnlock: Bool? = false
// To identify whether it is being unlocked...
var isUnlocking: Bool = false
}
struct UnlockingCharacters: Identifiable, Codable {
var id = UUID().uuidString
var character: Character
}
class SharedDataModel: ObservableObject {
// Unlocking Characters...
#Published var unlockingCharacters: [Character] = []
}
My functions:
func isUnlocked() -> Bool {
return sharedData.unlockingCharacters.contains { characterData in
return self.characterData.id == characterData.id
}
}
func addToUnlocking() {
if let index = sharedData.unlockingCharacters.firstIndex(where: {
characterData in
return self.characterData.id == characterData.id
}){
// Remove from unlocking...
sharedData.unlockingCharacters.remove(at: index)
}
else {
// Add to unlocking...
sharedData.unlockingCharacters.append(characterData)
}
}
And my UserModel:
struct UserModel: Identifiable, Codable {
var username : String
var pic : String
var bio: String
var uid : String
var id: String { uid }
var activeUnlockingCharacters: [UnlockingCharacters]
}
When trying to process the custom object I get errors:
let ref = Firestore.firestore()
func fetchUser(uid: String,completion: #escaping (UserModel) -> ()){
let db = Firestore.firestore()
ref.collection("Users").document(uid).getDocument { (doc, err) in
guard let user = doc else{return}
let username = user.data()?["username"] as? String ?? "No Username"
let pic = user.data()?["imageurl"] as? String ?? "No image URL"
let bio = user.data()?["bio"] as? String ?? "No bio"
let uid = user.data()?["uid"] as? String ?? ""
do {
try db.collection("Users").document("\(uid)").setData(from: UnlockingCharacters)
} catch let error {
print("Error writing object to Firestore: \(error)")
}
DispatchQueue.main.async {
completion(UserModel(username: username, pic: pic, bio: bio, uid: uid, activeUnlockingCharacters: UnlockingCharacters))
}
}
}
I also get errors in the following line inside my ProfileViewModel:
#Published var userInfo = UserModel(username: "", pic: "", bio: "", uid: "", activeSupportingCharities: [SupportingCharities])
The errors:
Missing argument for parameter 'activeUnlockingCharacters' in call
Cannot convert value of type '[UnlockingCharacters].Type' to expected argument type '[UnlockingCharacters]'
Here is my data structure in the firebase console:
I want there to be a field called UnlockingCharacters in the users data model on firebase when a character is added to the UnlockingCharacters struct.
I think the issue is that your code for writing back to the User document doesn't refer to an instance of UnlockingCharacters , but instead to the type UnlockingCharacters.
So this line:
try db.collection("Users").document("\(uid)").setData(from: UnlockingCharacters)
should probably(*) become
let userModel = UserModel(username: username, pic: pic, bio: bio, uid: uid, activeUnlockingCharacters: unlockedCharacters)
try db.collection("Users").document("\(uid)").setData(from: userModel)
*: probably, because I wasn't sure about your data structure. You might want to post a screenshot of your Firestore data model (in the console) to make it easier to understand how you're intending to store this data.
Also, two other notes:
You probably want to use Codable to replace the manual mapping (let username = user.data()?["username"] as? String ?? "No Username" etc.)
no need to wrap the UI update in DispatchQueue.main.async - Firestore calls back on the main thread already - see https://twitter.com/peterfriese/status/1489683949014196226 .
How do I take the name of the Firebase document, directly from the information of the logged in user?
Basically I have a user collection and a Degree Course collection.
When I use the func
GetCorsodiLaurea
I don't want to manually insert the document name in .document ("")
But I would like to automatically take the name of the document directly from the user's info
The field that declares which course is connected to the user is "TipoCorso".
As you can see in the Degree Courses collection there is the value of the "TipoCorso" field
Here is the code of the function and a screen of the Firebase Database:
import SwiftUI
import Firebase
import FirebaseFirestoreSwift
import FirebaseDatabase
class ProfileViewModel : ObservableObject{
#Published var userInfo = UserModel(Nome: "", Cognome: "", photoURL: "", Nomeintero: "", Corsodilaurea: "", Tipocorso: "")
#Published var userDegree = userDegreeModel(Name: "", TotalSubjects: "")
var ref = Firestore.firestore()
let uid = Auth.auth().currentUser!.uid
let db = Firestore.firestore()
init() {
fetchUser()
GetCorsodiLaurea()
}
func fetchUser() {
ref.collection("users").document(uid).getDocument { [self] (doc, err) in guard let user = doc else { return }
let Nome = user.data()?["Nome"] as! String
let Cognome = user.data()?["Cognome"] as! String
let photoURL = user.data()?["photoURL"] as! String
let Nomeintero = user.data()?["Nomeintero"] as! String
let Tipocorso = user.data()?["Tipocorso"] as! String
let Corsodilaurea = user.data()?["Corsodilaurea"] as! String
DispatchQueue.main.async {
self.userInfo = UserModel(Nome: Nome, Cognome: Cognome, photoURL: photoURL, Nomeintero: Nomeintero, Corsodilaurea: Corsodilaurea, Tipocorso: Tipocorso) }
}
}
func GetCorsodiLaurea() {
db.collection("DegreeCourses").document(self.userInfo.Tipocorso).getDocument { [self] (doc, err) in guard let DegreeCourses = doc else { return }
let Name = DegreeCourses.data()?["Name"] as! String
let TotalSubjects = DegreeCourses.data()?["TotalSubjects"] as! String
// [END doc_reference]
// [END collection_reference]
DispatchQueue.main.async {
self.userDegree = userDegreeModel(Name: Name, TotalSubjects: TotalSubjects)
}
}
}
}
User
DegreeCourses
When you call the fetchUeser() function it looks like you are populating the UserModel with the specific user's Tipocorso.
So in the GetCorsodiLaurea function you can call Tipocorso member in userInfo variable.
ref.collection("DegreeCourses").document(self.userInfo.Tipocorso).getDocument { [self] (doc, err) in guard let DegreeCourses = doc else { return }
Edit: You are most likely getting the error because the fetchUsers() function hasn't completed fully (as it is waiting for Firebase to respond) but the execution has already proceeded to the GetCorsodiLaurea() function.
To fix this add, a escaping closure to the fetchUsers() function and call the GetCorsodiLaurea() function in the closure. This way, the compiler won't try and execute the functions asynchronously.
import SwiftUI
import Firebase
import FirebaseFirestoreSwift
import FirebaseDatabase
class ProfileViewModel : ObservableObject{
#Published var userInfo = UserModel(Nome: "", Cognome: "", photoURL: "", Nomeintero: "", Corsodilaurea: "", Tipocorso: "")
#Published var userDegree = userDegreeModel(Name: "", TotalSubjects: "")
var ref = Firestore.firestore()
let uid = Auth.auth().currentUser!.uid
let db = Firestore.firestore()
init() {
//GetCorsodilaurea() will only get called after fetchUser() is complete
fetchUser(completion: {
GetCorsodiLaurea()
})
}
func fetchUser(completion: #escaping () -> Void)) {
ref.collection("users").document(uid).getDocument { [self] (doc, err) in guard let user = doc else { return }
let Nome = user.data()?["Nome"] as! String
let Cognome = user.data()?["Cognome"] as! String
let photoURL = user.data()?["photoURL"] as! String
let Nomeintero = user.data()?["Nomeintero"] as! String
let Tipocorso = user.data()?["Tipocorso"] as! String
let Corsodilaurea = user.data()?["Corsodilaurea"] as! String
//don't do this async
self.userInfo = UserModel(Nome: Nome, Cognome: Cognome, photoURL: photoURL, Nomeintero: Nomeintero, Corsodilaurea: Corsodilaurea, Tipocorso: Tipocorso)
completion()
}
}
func GetCorsodiLaurea() {
db.collection("DegreeCourses").document(self.userInfo.Tipocorso).getDocument { [self] (doc, err) in guard let DegreeCourses = doc else { return }
let Name = DegreeCourses.data()?["Name"] as! String
let TotalSubjects = DegreeCourses.data()?["TotalSubjects"] as! String
// [END doc_reference]
// [END collection_reference]
DispatchQueue.main.async {
self.userDegree = userDegreeModel(Name: Name, TotalSubjects: TotalSubjects)
}
}
}
My Firestore data is set up like this:
This is how I'm reading the data:
for doc in snapshot!.documents {
let recipeFromFirestore = Recipe(
glutenFree: doc["glutenFree"] as! Bool,
dairyFree: doc["dairyFree"] as! Bool,
cheap: doc["cheap"] as! Bool)
recipes.append(recipeFromFirestore)
}
These are my Recipe and ExtendedIngredients structs:
struct Recipe: Codable {
var glutenFree: Bool?
var dairyFree: Bool?
var cheap: Bool?
var extendedIngredients: [ExtendedIngredients]? = nil
}
struct ExtendedIngredients: Codable {
var aisle: String?
var image: String?
var name: String?
var amount: Double?
var unit: String?
}
How can I go about reading the array of Map type data in my extendedIngredients field in Firestore? I'm not sure how to include that in my let recipeFromFirestore code.
Any help or guidance is much appreciated!
I was able to get all of my data, including the Map type by using the Codable API.
docRef.getDocument { document, error in
if let error = error as NSError? {
self.errorMessage = "Error getting document: \(error.localizedDescription)"
}
else {
if let document = document {
do {
self.recipe = try document.data(as: Recipe.self)
let recipeFromFirestore = Recipe(
glutenFree: self.recipe!.glutenFree,
dairyFree: self.recipe!.dairyFree,
cheap: self.recipe!.cheap,
extendedIngredients: self.recipe!.extendedIngredients)
self.recipes.append(recipeFromFirestore)
}
catch {
print("Line 136: \(error)")
}
}
}
}
I did not need to do any explicit mapping with this approach.
I have the Url saved in the info.plist as such:
BASE_URL <-> String <-> $(BASE_URL)
and in my project's Build Settings, I added a user-defined setting as such:
BASE_URL http://data.nba.net
After setting this up, when I try to get the website into the url variable, the variable returns "". As I debug the issue, I don't see the website stored under that variable.
I am new to Swift and still learning so any comments on the way I have setup my structs will be appreciated as well.
import UIKit
struct sports_content: Decodable {
let sports_meta_expanded: sports_meta
let teams_expanded: teams
}
struct sports_meta: Decodable {
let date_time: String?
let season_meta_list: season_meta
}
struct season_meta: Decodable {
let calendar_date: Date
let season_year: Int?
let stats_season_year: Int?
let stats_season_id: Int?
let stats_season_stage: Int?
let roster_season_year: Int?
let schedule_season_year: Int?
let standings_season_year: Int?
let season_id: Int?
let display_year: String
let display_season: String
let season_stage: Int?
}
struct next: Decodable {
let url: String
}
struct teams: Decodable {
let season_year: year
let team_data: [team]
}
struct year: Decodable {
let season_year: Int?
}
struct team: Decodable {
let is_nba_team: Bool
let team_name: String
let team_nickname: String
let team_code: String
let team_abbrev: String
let city: String
let state: String
let team_short_name: String
let team_id: Int?
let conference: String
let division_id: String
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let url = Bundle.main.infoDictionary?["BASE_URL"] as? String ?? ""
guard let convertedURL = URL(string: url) else {
return
}
URLSession.shared.dataTask(with: convertedURL) { (data, response, error) in
guard let data = data else {
return
}
do{
let dataSet = try JSONDecoder().decode(sports_content.self, from: data)
print(dataSet)
} catch {
print("JSONSerialization error:", error)
}
}.resume()
}
}
A build setting is used at build / compile time and not necessarily at run time.
To get your URL into the infoDictionary, you need to add it to the Info.plist file. Double click on your Info.plist to get the view open in your Xcode, then click "Add Value" under the Editor menu, then you can add BASE_URL as the key and your URL as the value.
Try using $(BASE_URL) as the value in your Info.plist and see if your build setting gets added in at build time. :)
I have a user model as per the below which gives me a dictionary of a User.
import UIKit
import FirebaseFirestore
protocol SerializeUser {
init?(dictionary: [String: Any])
}
struct User {
var documentRef: DocumentReference?
var displayName: String
var photoURL: String
var email: String
var isEmployee: Bool
var dictionary: [String: Any] {
return ["displayName": displayName, "photoURL": photoURL, "email": email, "isEmployee": isEmployee]
}
}
extension User: SerializeUser {
init?(dictionary: [String: Any]) {
guard
let displayName = dictionary["displayName"] as? String,
let photoURL = dictionary["photoURL"] as? String,
let isEmployee = dictionary["isEmployee"] as? Bool,
let email = dictionary["email"] as? String else { return nil }
self.init(displayName: displayName, photoURL: photoURL, email: email, isEmployee: isEmployee)
}
}
I need to initialize the documentRef within my User struct somehow any ideas on how to do that? please.
Somewhere you need an instance of your database to build the document reference.
var db: Firestore? = Firestore.firestore()
struct User {
lazy var documentRef: db.collection("users").document(displayName)
}
I don't see a document id referenced anywhere here so I assumed you're using displayName as a key. Declaring the variable as lazy lets you initialize it with your other instance variables.
Hope this helps.
I have solved this by using Decodable Protocol as illustrated in this post;
Using Decodable to get the object and document ID with Firestore