Struggling to pass a single document through Firestore - Swift - ios

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

Related

Swift Firebase Processing A Custom Object

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 .

Adding a local variable to downloaded MySQL Data with Models

I am using MySQL and PHP to download a restaurants menu but the user of the app should be able to add a certain amount to which item from the menu they want. Currently I am using a stepper to indicate the amount and adding that amount to a UserDefaults key which gets called when the menu is downloaded again.
This makes me have to download the menu again when I go to another viewController which sums up the order but I can't seem to filter out only them items which do have an amount.
What is a better way to add that amount to the downloaded data and how can I filter these items in my cart ViewController to only show and use the items which have an amount.
My current downloadModel, MenuModel, cellViewController (for the menu tableview) look like this:
MenuDownload.swift:
import UIKit
protocol MenuDownloadProtocol: class {
func productsDownloaded(products: NSArray)
}
class MenuDownload: NSObject {
//properties
weak var delegate: MenuDownloadProtocol!
func downloadProducts() {
let urlPath = "http://server.com/download.php" // Fake URL obviously
let url: URL = URL(string: urlPath)!
let defaultSession = Foundation.URLSession(configuration: URLSessionConfiguration.default)
let task = defaultSession.dataTask(with: url) { (data, response, error) in
if error != nil {
print("Failed to download data")
}else {
print("Menu downloaded")
self.parseJSON(data!)
}
}
task.resume()
}
func parseJSON(_ data:Data) {
var jsonResult = NSArray()
do{
jsonResult = try JSONSerialization.jsonObject(with: data, options:JSONSerialization.ReadingOptions.allowFragments) as! NSArray
} catch let error as NSError {
print(error)
}
var jsonElement = NSDictionary()
let products = NSMutableArray()
for i in 0 ..< jsonResult.count
{
jsonElement = jsonResult[i] as! NSDictionary
let restomenu = MenuModel()
//the following insures none of the JsonElement values are nil through optional binding
if let product = jsonElement["product"] as? String,
let price = jsonElement["price"] as? String,
let info = jsonElement["info"] as? String,
let imageurl = jsonElement["imageurl"] as? String
{
let productandprice = product + " " + "€" + price
let quantityy = UserDefaults.standard.object(forKey: productandprice) as? String
restomenu.product = product
restomenu.price = price
restomenu.info = info
restomenu.imageurl = imageurl
restomenu.quantity = quantityy
}
products.add(restomenu)
}
DispatchQueue.main.async(execute: { () -> Void in
self.delegate.productsDownloaded(products: products)
})
}
}
extension String {
func chopPrefix(_ count: Int = 1) -> String {
return substring(from: index(startIndex, offsetBy: count))
}
func chopSuffix(_ count: Int = 1) -> String {
return substring(to: index(endIndex, offsetBy: -count))
}
}
MenuModel.swift:
import UIKit
class MenuModel: NSObject {
//properties
var product: String?
var price: String?
var info: String?
var imageurl: String?
var quantity: String?
//empty constructor
override init()
{
}
init(product: String, price: String, info: String, imageurl: String, quantity: String) {
self.product = product
self.price = price
self.info = info
self.imageurl = imageurl
self.quantity = quantity
}
//prints object's current state
override var description: String {
return "product: \(String(describing: product)), price: \(String(describing: price)), info: \(String(describing: info)), imageurl: \(String(describing: imageurl)), quantity: \(String(describing: quantity))"
}
}
tableViewCell.swift:
import UIKit
class productTableViewCell: UITableViewCell {
#IBOutlet weak var productLabel: UILabel!
#IBOutlet weak var productImage: UIImageView!
#IBOutlet weak var cellView: UIView!
#IBOutlet weak var orderCount: UILabel!
#IBOutlet weak var stepper: UIStepper!
var amount: String?
#IBAction func stepperValueChanged(_ sender: UIStepper) {
amount = Int(sender.value).description
orderCount.text = amount
// let defaultkey = String(productLabel.text!)
UserDefaults.standard.setValue(amount, forKey: productLabel.text!)
if amount == "0"
{
orderCount.isHidden = true
UserDefaults.standard.removeObject(forKey: productLabel.text!)
}
else
{
orderCount.isHidden = false
}
}
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
EDIT: after trying filtering options and many different ways I still haven't found how to fix this. I think I'm overthinking it too much.

How can I connect a post to user and display that user and post data in a Custom cell in TableView using Firebase

I am building a social media app and since I wasn't able to solve this by myself, I need help.
I have connected my Xcode project to the Firebase and made it possible for my users to register/sign in and publish Posts to the Firebase which are then shown all together in one group TableView but none of the data is connected to the user which posted that Post. The idea is that it looks similar to Instagram posts, but every post in my app would have to include only: Photo and Caption(optional) which are part of a Post Class, and CraftName which is a part of User Class. I believe that the problem lies in "denormalization" and incorrect populating of tableView.
Here is a photo of my Firebase tree which currently has 2 users signed in. One has posted 1 Post, and another has posted 2 Posts
https://i.imgur.com/fc2LEMk.jpg
I successfully register Users( with email, username and CraftName) to firebase database and I have made it possible for them to sign in and sign out so I believe the problem is somewhere else to look. I have also made it possible to post a Post to Firebase which includes Photo and Caption and to populate the tableView with that two objects. Only thing left is to connect user with it's Post and while displaying the Post, provide that User's CraftName as a TextView above the Post.
This is AuthService class which deals with registration
static func register(username: String, email: String, password: String, craftName: String, onSuccess: #escaping () -> Void, onError: #escaping (_ errorMessage: String?) -> Void) {
Auth.auth().createUser(withEmail: email, password: password, completion: { (user, error) in
if error != nil {
onError(error!.localizedDescription)
return
}
let uid = Auth.auth().currentUser!.uid
self.setUserInformation(username: username, email: email, craftName: craftName, uid: uid, onSuccess: onSuccess)
})
}
This is a registration class
#IBAction func registerButtonPressed(_ sender: Any) {
view.endEditing(true)
AuthService.register(username: usernameTextField.text!, email: emailTextField.text!, password: passwordTextField.text!, craftName: craftNameTextField.text!, onSuccess: {
self.performSegue(withIdentifier: "registerToTabBar", sender: self)
}, onError: {error in
ProgressHUD.showError(error!)
})
}
}
//Stvaranje reference na Firebase Realtime bazu podataka i spremanje svih podataka svakog korisnika zasebno u tu bazu podataka
static func setUserInformation(username: String, email: String, craftName: String, uid: String, onSuccess: #escaping () -> Void){
let ref = Database.database().reference()
let usersReference = ref.child("users")
let newUserReference = usersReference.child(uid)
newUserReference.setValue(["username": username, "email": email, "craftName": craftName])
onSuccess()
}
}
This is my User Class
struct User {
var username: String?
var email: String?
var craftName: String
init(craftNameString: String, emailString : String, usernameString: String){
craftName = craftNameString
username = usernameString
email = emailString
}
}
This is my Post Class
class Post {
var caption: String?
var photoUrl: String?
var numberOfClaps: Int?
init(captionText: String, photoUrlString: String) {
caption = captionText
photoUrl = photoUrlString
}
}
This is my Post custom cell class
class PostCell: UITableViewCell {
#IBOutlet weak var postImageView: UIImageView!
#IBOutlet weak var captionTextViev: UITextView!
#IBOutlet weak var craftNameTextField: UITextView!
var post : Post! {
didSet{
self.updatePostUI()
}
}
var user : User! {
didSet{
self.updateUserUI()
}
}
func updatePostUI() {
captionTextViev.text = post.caption
}
func updateUserUI(){
craftNameTextField.text = user.craftName
}
}
Now when my users are Registered, they can post a Post in CameraClass.
#IBAction func shareButtonPressed(_ sender: Any) {
if let postImg = selectedImage, let imageData = postImg.jpegData(compressionQuality: 1) {
let photoIDString = NSUUID().uuidString
print(photoIDString)
let storageRef = Storage.storage().reference(forURL: Config.STORAGE_ROOT_REF).child("posts").child(photoIDString)
storageRef.putData(imageData, metadata: nil, completion: { (metadata, error) in
if error != nil {
ProgressHUD.showError(error?.localizedDescription)
return
}
storageRef.downloadURL(completion: { (url, error) in
if error != nil {
ProgressHUD.showError(error!.localizedDescription)
}else {
if let photoURL = url?.absoluteString{
self.sendDataToDatabase(photoUrl: photoURL)
}
}
})
})
}
}
func sendDataToDatabase(photoUrl: String) {
let ref = Database.database().reference()
let uid = Auth.auth().currentUser!.uid
let postsReference = ref.child("posts")
let userUID = postsReference.child(uid)
let newPostID = userUID.child(userUID.childByAutoId().key!)
newPostID.setValue(["photoUrl": photoUrl, "caption": captionTextView.text!], withCompletionBlock: { (error, ref) in
if error != nil {
ProgressHUD.showError(error!.localizedDescription)
return
}
ProgressHUD.show("Uspješno ste objavili fotografiju")
ProgressHUD.dismiss()
self.clean()
self.tabBarController?.selectedIndex = 0
})
This is my HomeViewController in which is the tableView that is supposed to display Posts
class HomeViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
var posts = [Post]()
struct Storyboard {
static let postCellDefaultHeight : CGFloat = 578.0
}
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.estimatedRowHeight = Storyboard.postCellDefaultHeight
tableView.rowHeight = UITableView.automaticDimension
tableView.separatorColor = UIColor.clear
loadPosts()
}
//Function which is supposed to retrieve data from database and populate the TableView
func loadPosts() {
let ref = Database.database().reference()
let posts = ref.child("posts")
posts.observe(.value) { (snapshot) in
for currentUser in (snapshot.children) {
let cUSer = currentUser as! DataSnapshot
for postInfo in (cUSer.children) {
let postSnap = postInfo as! DataSnapshot
let dict = postSnap.value as? [String: Any]
let captionText = dict!["caption"] as! String
let photoUrlString = dict!["photoUrl"] as! String
let post = Post(captionText: captionText, photoUrlString: photoUrlString)
self.posts.append(post)
self.tableView.reloadData()
}
}
}
}
It looks like this right now:
https://i.imgur.com/lMxZYLW.jpg
This is how I would like it to look:
https://i.imgur.com/721mEXm.jpg

Trouble passing data

I’m having trouble passing data via prepareForSegue from MainVC to EditVC. The gist of the problem is I’m trying to edit quote info(quote and author) already entered(saved in a Firestore database). When swiping(UIContextualAction) and tap on edit, it’s suppose to pass the data on to the EditVC where it’ll put the quote text and author text in their own UITextView where it’s editable. Once you edit the text, hit save and it’ll update the entry in Firestore; then MainVC reloads to update it’s view. The segue to the EditVC works flawlessly but it doesn’t display the quote & author text in their respective text views. I’ve been banging my head against the wall for 3 days trying to figure it out. Any help or guidance from you all is greatly appreciated. I can provide the Github link upon request.
MainVC:
}
let edit = UIContextualAction(style: .normal, title: "Edit") { (action, view, nil) in
self.performSegue(withIdentifier: "toEditQuote", sender: self.quotes)
print("Segue initiated")
}
edit.backgroundColor = #colorLiteral(red: 0.1764705926, green: 0.4980392158, blue: 0.7568627596, alpha: 1)
return UISwipeActionsConfiguration(actions: [delete, edit])
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?){
print("preparing to segue")
if let destination = segue.destination as? EditQuoteVC {
print("Destination = EditQuoteVC")
if let quoteData = sender as? Quote {
print("data passing")
destination.quoteTxt = quoteData.quoteTxt
destination.authorTxt = quoteData.authorTxt
print("data passed")
}
}
}
}
EditQuoteVC(destination)
class EditQuoteVC: UIViewController {
//Outlets
#IBOutlet weak var quoteText: UITextView!
#IBOutlet weak var authorText: UITextView!
//Variables
var quoteTxt: String!
var authorTxt: String!
override func viewDidLoad() {
super.viewDidLoad()
quoteText.text = quoteTxt
authorText.text = authorTxt
}
Quote.swift
class Quote {
private(set) var quoteTxt: String!
private(set) var authorTxt: String!
private(set) var timestamp: Date!
private(set) var userId: String!
private(set) var documentID: String!
init(quote: String, author: String, timestamp: Date, userId: String, documentID: String) {
self.quoteTxt = quote
self.authorTxt = "- \(author)"
self.timestamp = timestamp
self.userId = userId
self.documentID = documentID
}
class func parseData(snapshot: QuerySnapshot?) -> [Quote] {
var quotes = [Quote]()
guard let snap = snapshot else { return quotes}
for document in snap.documents { //Grabs the documents...
let data = document.data()
let quote = data[QUOTE_TEXT] as? String ?? ""
let author = data[AUTHOR_TEXT] as? String ?? ""
let timestamp = data[TIMESTAMP] as? Date ?? Date()
let userId = data[USER_ID] as? String ?? ""
let documentID = document.documentID
let newQuote = Quote(quote: quote, author: author, timestamp: timestamp, userId: userId, documentID: documentID)
quotes.append(newQuote)
}
return quotes
}
}
In your prepareForSegue function, you are assigning destination to sender. It should be:
if let destination = segue.destination as? EditQuoteVC

How do I get Firebase database value into UILabel text?

I'm trying to read a value from my Firebase database. I then want to change UILabel text to the database child value. Seems pretty simple, but I cannot figure out why the value is reading blank.
Here is my Firebase JSON:
{
"pilots" : {
"HpPzn0XUqMgsKhUOpH75lHIhyFA3" : {
"pilot" : "First Lastname",
"weight" : 180
}
}
}
Here are the Firebase rules, just for testing at the moment:
{
"rules": {
"pilots": {
".read": true,
".write": true
}
}
}
Finally the Swift 3 code, which is probably ugly as sin. First app after reading and online lessons.
import UIKit
import Firebase
import FirebaseCore
import FirebaseAuth
import FirebaseDatabase
class MainMenuViewController: UIViewController {
#IBOutlet weak var pilotUsername: UILabel!
#IBOutlet weak var dateTime: UILabel!
#IBOutlet weak var aircraftLabel: UILabel!
#IBOutlet weak var riskScoreInt: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let ref = FIRDatabase.database().reference()
let userID = FIRAuth.auth()?.currentUser?.uid
ref.child("pilots").child(userID!).child("pilot").observeSingleEvent(of: .value, with: { (snapshot) in
// Get user value
let value = snapshot.value as? NSDictionary
let username = value?["username"] as? String ?? ""
self.pilotUsername.text = username
print(username)
// ...
}) { (error) in
print(error.localizedDescription)
}
}
}
I'm just using the example code from the Firebase documentation. There's a line of code (in the example) after
let username = value?["username"] as? String ?? ""
that is :
let user = User.init(username: username)
but it gives me an error. "Use of unresolved identifier 'User'"
I don't think I need that line of code, since nothing like it is used in the examples and lessons that I've folowed.
Thank's in advance. This is my first time posting to Stack Overflow.
import FirebaseDatabase
class User {
// MARK: Properties
var firstname: String
var lastname: String
var username: String { return "\(firstname)\(lastname)" }
// MARK: Initializers
init(firstname: String, lastname: String) {
self.firstname = firstname
self.lastname = lastname
}
init?(snapshot: FIRDataSnapshot) {
guard
let firstname = snapshot.childSnapshot(forPath: "First").value as? String,
let lastname = snapshot.childSnapshot(forPath: "Lastname").value as? String
else { return nil }
self.firstname = firstname
self.lastname = lastname
}
}
Then
ref.child("pilots").child(userID!).child("pilot").observeSingleEvent(of: .value, with: { [weak self] (snapshot) in
if let user = User(snapshot: snapshot) {
self?.pilotUsername.text = user.username
}
}) { (error) in
print(error.localizedDescription)
}

Resources