Swift Firebase Processing A Custom Object - ios

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 .

Related

Error using Codable to map user data in swift

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?

Does not conform to protocol Decodabel and Encodable

Can someone tell me what's wrong with my approach? the error is
Type 'User' does not conform to protocol 'Decodable'
Type 'User' does not conform to protocol 'Encodable'
I have tried to replace the null string for Var id, pushId and avatarLink with String.self but no avail either.
Please help
struct User: Codable, Equatable{
var id = ""
var username = String.self
var email = String.self
var pushId = ""
var avatarLink = ""
var status = String.self
static var currentId: String {
return Auth.auth().currentUser!.uid
}
static var currentUser: User? {
if Auth.auth().currentUser != nil {
if let dicctionary = UserDefaults.standard.data(forKey: kCURRENTUSER) {
let decoder = JSONDecoder()
do {
let userObject = try decoder.decode(User.self, from: dicctionary)
return userObject
} catch {
print("Error decoding user from user defaults ", error.localizedDescription)
}
}
}
return nil
}
static func == (lhs: User, rhs: User) -> Bool {
lhs.id == rhs.id
}
}
When you write var username = String.self, the type of the username property will be not String, but String.Type. Basically, it holds not a string, but a type. A type itself is not encodable or decodable, and because of that the whole struct can't be implicitly codable.
If you want username, email and status to contain strings, but not types, but don't want them to have a default value of an empty string (like id or pushId), just declare them as follows: var username: String.
That will enable Swift compiler to synthesize the Codable conformance for you.

Struggling to pass a single document through Firestore - Swift

Here is my customer class:
class Customer {
// Creating a customer
let name: String
let surname: String
let contactNo: String
let email: String
init(name: String,surname: String,contactNo: String,email: String) {
self.name = name
self.surname = surname
self.contactNo = contactNo
self.email = email
}
}
This is the code I'm using which keeps returning a nil:
class ProfileCus: UIViewController {
// Labels to display data
#IBOutlet weak var nameLabel: UILabel!
#IBOutlet weak var surnameLabel: UILabel!
#IBOutlet weak var emailLabel: UILabel!
#IBOutlet weak var contactLabel: UILabel!
// Reference to customer collection in Firestore
private var customerRefCollection = Firestore.firestore().collection("customers")
// Customer Object
private var customer = Customer(name: "a",surname: "a",contactNo: "a",email: "a")
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
getDataFromFirebase{
self.customerRefCollection = Firestore.firestore().collection("customers")
print(self.customer,"debug step 5")
self.nameLabel.text = self.customer.name
self.surnameLabel.text = self.customer.surname
self.emailLabel.text = self.customer.email
self.contactLabel.text = self.customer.contactNo
}
}
func getDataFromFirebase(completion:#escaping() -> ()){
print(self.customer,"debug step 1")
let userID = Auth.auth().currentUser?.uid
print(userID,"debug step 2")
// Locate the user information on Firestore
customerRefCollection.document(userID!).getDocument { (snapshot, error) in
if let err = error {
debugPrint("Error fetching documents: \(err)")
}
else {
// Ensure that if there's nothing in the document that the function returns
guard let snap = snapshot else {return}
print(snap, "debug step 3")
// Parse the data to the customer model
let data = snap.data()
let name = data?["name"] as? String ?? ""
let surname = data?["surname"] as? String ?? ""
let email = data?["email"] as? String ?? ""
let contact = data?["contact no"] as? String ?? ""
// Create the customer and pass it to the global variable
let cus = Customer(name: name, surname: surname, contactNo: contact, email: email)
print(self.customer,"debug step 4")
self.customer = cus
}
completion()
}
}
}
Can anyone please help me understand what I am doing wrong because the snapshot does return but the way I parse the data is wrong because the customer object returns a nil.
I have added print statements with tags saying debug step 1 ect so you can follow what happens at run time, here is the output:
020-08-13 21:15:20.388052+0200 Clean Wheels[8599:430648] 6.29.0 - [Firebase/Analytics][I-ACS023012] Analytics collection enabled
Customer(name: "a", surname: "a", contactNo: "a", email: "a") debug step 1
Optional("RWVTDIUuL1eahOLpZT1UmMl0cja2") debug step 2
<FIRDocumentSnapshot: 0x6000017499f0> debug step 3
Customer(name: "a", surname: "a", contactNo: "a", email: "a") debug step 4
Customer(name: "", surname: "", contactNo: "", email: "") debug step 5
It seems to me as if the data function is not the correct function to use because when I hard code the values its shows up in the UI Profile View, is there perhaps an alternative?
Output once the code runs
There are a number of ways you can do this but what I'd suggest is passing the customer object through the completion handler (to the caller). You could also configure the customer object to take the document snapshot in its initializer (instead of taking 4 separate properties) and either return a customer object or nil (this would require a failable intializer which is incredibly basic). Also, I didn't see a need to declare so many instance properties (in this example, anyway) so I took them out. I also made the customer number an integer, not a string (to illustrate how I would structure the data).
class Customer {
let name: String
let surname: String
let contactNo: Int // change this back to a string
let email: String
init(name: String, surname: String, contactNo: Int, email: String) {
self.name = name
self.surname = surname
self.contactNo = contactNo
self.email = email
}
}
class ProfileCus: UIViewController {
#IBOutlet weak var nameLabel: UILabel!
#IBOutlet weak var surnameLabel: UILabel!
#IBOutlet weak var emailLabel: UILabel!
#IBOutlet weak var contactLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
getCustomer { (customer) in
if let customer = customer {
print(customer)
} else {
print("customer not found")
}
}
}
private func getCustomer(completion: #escaping (_ customer: Customer?) -> Void) {
guard let userID = Auth.auth().currentUser?.uid else {
completion(nil)
return
}
Firestore.firestore().collection("customers").document(userID).getDocument { (snapshot, error) in
if let doc = snapshot,
let name = doc.get("name") as? String,
let surname = doc.get("surname") as? String,
let contact = doc.get("contact") as? Int, // cast this as a string
let email = doc.get("email") as? String {
let customer = Customer(name: name, surname: surname, contactNo: contact, email: email)
completion(customer)
} else {
if let error = error {
print(error)
}
completion(nil)
}
}
}
}

How to Initialize Firestore Document Reference Swift

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

What is the best way to convert JSON into swift object, Checking keys and values both

Ok I thought its not a major issue but I am wrong. Currently I am working on a project where I get huge chunk of JSON return. I am fetching those and making my model. Now in my model I am checking if there any value is nil by guard statement. Here is a sample of my model:
import Foundation
import SwiftyJSON
class profileModel {
var _phone_no: String?
var _full_name: String?
var _image: String?
var _email: String?
var _profile_pic: String?
var _rating: Int?
var _dob: String?
var _gender: String?
var _firstName: String?
var _lastName: String?
required init?(phone_no: String, full_name: String, image: String, email: String, profile_pic: String, rating: Int, dob: String, gender: String, firstName: String, lastName: String) {
self._phone_no = phone_no
self._full_name = full_name
self._image = image
self._email = email
self._profile_pic = profile_pic
self._rating = rating
self._dob = dob
self._gender = gender
self._firstName = firstName
self._lastName = lastName
}
convenience init?(json: JSON){
guard let phone_no = json["phone_no"].string,
let full_name = json["full_name"].string,
let image = json["profile_pic"].string,
let email = json["email"].string,
let profile_pic = json["profile_pic"].string,
let rating = json["rating"].int,
let dob = json["dob"].string,
let gender = json["gender"].string,
let firstName = json["first_name"].string,
let lastName = json["last_name"].string else {
print("Profile Detail Model Error")
return nil
}
self.init(phone_no: phone_no, full_name: full_name, image: image, email: email, profile_pic: profile_pic, rating: rating, dob: dob, gender: gender, firstName: firstName, lastName: lastName)
}
}
But how can I prevent crashes when any key is missing from JSON return? Seems like when I check both key & values the class got really really big, there must be some better way.
Making properties optionals is a good approach, however you can take advantage of the new Codable from Swift 4 where you can parse JSON to any data model that conformance to Codable.
In your case you can write the model like this:
class ProfileModel: Codable {
var phone_no: String?
var full_name: String?
var profile_pic: String?
var email: String?
// var profile_pic: String?
var rating: String?
var dob: String?
var gender: String?
var first_name: String?
var last_name: String?
}
And when you need to decode from the server use:
let profile = try JSONDecoder().decode(ProfileModel.self, from: json1)
If you get an array of "Profile" just change the above line to:
let profiles = try JSONDecoder().decode([ProfileModel].self, from: json1)
There is no need to use the library SwiftyJSON any more.
You should have a look at the Codable protocol: The following Playground shows what happens, when you try to parse a Json, that is missing a particular key.
//: Playground - noun: a place where people can play
import Foundation
At first, we create our ProfileModel class and mock a related json.
class ProfileModel: Codable {
//...
var _firstName: String?
var _lastName: String?
}
let profile = ProfileModel()
profile._firstName = "Hans"
profile._lastName = "Peter"
let json = try! JSONEncoder().encode(profile)
Parsing works as expected:
do {
let profileAgain = try JSONDecoder().decode(ProfileModel.self, from: json)
print(profileAgain._firstName) // "Optional("Hans")\n"
print(profileAgain._lastName) // "Optional("Peter")\n"
} catch {
print("something went wrong")
}
So what happens, when we add another property to our class (_phone_no), that is not included in our Json? Nothing really changes, if this new property is optional:
class AnotherProfileModel: Codable {
//...
var _firstName: String?
var _lastName: String?
var _phone_no: Int?
}
do {
let anotherProfile = try JSONDecoder().decode(AnotherProfileModel.self, from: json)
print(anotherProfile._firstName) // "Optional("Hans")\n"
print(anotherProfile._lastName) // "Optional("Peter")\n"
print(anotherProfile._phone_no) // "nil\n"
} catch {
print("something went wrong")
}
But if this property is not an optional, the decoder will throw an error:
class AndYetAnotherProfileModel: Codable {
//...
var _firstName: String?
var _lastName: String?
var _phone_no: Int
}
do {
let andYetAnotherProfileModel = try JSONDecoder().decode(AndYetAnotherProfileModel.self, from: json)
} catch {
print("something went wrong") // "something went wrong\n"
}
I hope this working example will help you, to get a better understanding of the Codable protocol :)

Resources