Firebase accessing sub-collection

@coder3000

Hi Gilbert,

Will do. I’ve managed to get code figured out to read back the pickup data but will tidy that up a bit before I share it with you. Got a busy day ahead at an event here in Perth so will get back to you maybe this evening or tomorrow morning my time. I’m up to my armpits in Alligators at the moment (just a tad busy).

1 Like

@coder3000

Hi Gilbert

I’ve managed to get code sorted to read the pickup data from a user and then save that data should the user want to edit those details on screen.

There are 3 functions

  • getUserDocumentId() - which locates the record for the current User.
  • get PickupDetails() - which retrieves the pickup data. In the ViewController I was testing this in, that data is displayed on screen in TextFields
  • saveChanges() - which saves the data should the user wish to edit the details.

I’m assuming that you have the following properties for the pickup data:

unit
street
city
state
country
postalCode
latitude
longitude
instructions

To read the data the first thing that has to be done is to identify the user record and get the user document ID.

This function searches the users collection and locates the record where the uid property is equal to the currentUser.uid. Having got that, it returns the document ID via a completion handler for that collection item.

    func getUserDocumentId(completion: @escaping (String) -> Void) {
        let db = Firestore.firestore()

        let users = db.collection("users")
        let query = users.whereField("uid", in: [Auth.auth().currentUser?.uid ?? ""])

        query.getDocuments { snapshot, error in
            if let error = error {
                self.showError(message: error.localizedDescription)
            } else if let snapshot = snapshot {
                for doc in snapshot.documents {
                    self.userDocId = doc.documentID
                    completion(doc.documentID)
                }
            }
        }
    }

In this function, the pickup details are then retrieved using that documentID by building a “path” to the correct pickup record.

    func getPickupDetails(docID: String) {
        let db = Firestore.firestore()

        //  Build the path to the
        let path = db.collection("users/\(docID)/pickup")

        path.getDocuments { snapshot, error in
            if let error = error {
                self.showError(message: error.localizedDescription)
            } else if let snapshot = snapshot {
                for doc in snapshot.documents {
                    //  Save the pickup document id.
                    self.pickupDocId = doc.documentID
                    //  Display the pickup details in the UI
                    self.unitTextField.text = doc["unit"] as? String
                    self.streetTextField.text = doc["street"] as? String
                    self.cityTextField.text = doc["city"] as? String
                    self.stateTextField.text = doc["state"] as? String
                    self.countryTextField.text = doc["country"] as? String
                    self.postalCodeTextField.text = doc["postalCode"] as? String
                    self.latitudeTextField.text = String(doc["latitude"] as? Double ?? 0)
                    self.longitudeTextField.text = String(doc["longitude"] as? Double ?? 0)
                    self.instructionsTextView.text = doc["instructions"] as? String
                }
            }
        }
    }

Declare two properties of the ViewController

    var userDocId: String = "" // Stores the user document id
    var pickupDocId: String = "" //  Stores the pickup document id

then call the getUserDocumentID which calls the getPickupDetails in the closure code.

getUserDocumentId { docID in
    // Save the document id to a variable to be used in the saveChanges function
    self.userDocId = docID
    //  Get the pickup data for the user
    self.getPickupDetails(docID: docID)
}

Let’s say the user edits the pickup details on the screen and saves the updated data.

I have a button in the View Controller:

    @IBAction func saveChangesTapped(_ sender: Any) {

        let error = validateFields()
        guard error == nil else {
            showError(message: error!)
            return
        }

        saveChanges()
    }

validateFields() function which returns a String the same as the validateFields() function in the SignUp View Controller.

    func validateFields() -> String? {

        // 2. Check that all fields are filled in
        if unitTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines) == "" ||
            streetTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines) == "" ||
            cityTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines) == "" ||
            stateTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines) == "" ||
            countryTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines) == "" ||
            postalCodeTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines) == "" ||
            latitudeTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines) == "" ||
            longitudeTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines) == "" {

            return "Please fill in all fields. (Instructions are optional)"
        }

        return nil
    }

then the saveChanges() function:

    func saveChanges() {
        let db = Firestore.firestore()

        //  Build the path to save the changes made to any of the pickup fields.
        let pickupData = db.collection("users").document(userDocId).collection("pickup").document(pickupDocId)

        var instructions = ""
        let unit = unitTextField.text!.trimmingCharacters(in: .whitespacesAndNewlines)
        let street = streetTextField.text!.trimmingCharacters(in: .whitespacesAndNewlines)
        let city = cityTextField.text!.trimmingCharacters(in: .whitespacesAndNewlines)
        let state = stateTextField.text!.trimmingCharacters(in: .whitespacesAndNewlines)
        let country = countryTextField.text!.trimmingCharacters(in: .whitespacesAndNewlines)
        let postalCode = postalCodeTextField.text!.trimmingCharacters(in: .whitespacesAndNewlines)
        let latitude = latitudeTextField.text!.trimmingCharacters(in: .whitespacesAndNewlines)
        let longitude = longitudeTextField.text!.trimmingCharacters(in: .whitespacesAndNewlines)
        if instructionsTextView.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
            instructions = "None"
        } else {
            instructions = instructionsTextView.text.trimmingCharacters(in: .whitespacesAndNewlines)
        }

        pickupData.updateData(["unit": unit,
                               "street": street,
                               "city": city,
                               "state": state,
                               "country": country,
                               "postalCode": postalCode,
                               "latitude": Double(latitude) as Any,
                               "longitude": Double(longitude) as Any,
                               "instructions": instructions ])
    }

Note the line that says:
let pickupData = db.collection("users").document(userDocId).collection("pickup").document(pickupDocId)

The path to the pickup data is the “users” collection followed by the document Id for that user followed by the “pickup” collection and then the document id for that collection item.

I hope you can make sense of all that.

1 Like

Hey Chris! Everything worked great! I’ll be making modifications but you gave me a great jumping off point! I can now expand upon this, seeing the possibilities that you have shared. Thanks again! You are brilliant!

1 Like

Hey @Chris_Parker,

I have been experimenting. Trying to gain a grasp on using an api call to work with UILables. I am having issue, but I feel like I’m getting close. Could you help with the code I have. I just realized I didn’t put the ability to edit a “Get” or “Post” when I need. If you could add that and if we could just get brandName to work, I will attempt the rest:

import UIKit

class ViewController: UIViewController {
    
    @IBOutlet var brandName: UILabel!
//    @IBOutlet var unit: UILabel!
//    @IBOutlet var street: UILabel!
//    @IBOutlet var city: UILabel!
//    @IBOutlet var state: UILabel!
//    @IBOutlet var postalCode: UILabel!
//    @IBOutlet var country: UILabel!
//    @IBOutlet var latitude: UILabel!
//    @IBOutlet var longitude: UILabel!
//    @IBOutlet var instructions: UILabel!

    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        //fetchData()
       
    }


}

func fetchData(brandName: UILabel!) {
    
    // MARK: - DeliveryOffer
    struct DeliveryOffer: Codable {
        let offers: [Document]
        
        private enum CodingKeys: String, CodingKey {
            case offers = "documents"
        }
    }

    // MARK: - Document
    struct Document: Codable {
        //let name: String
        let fields: Fields
        //let createTime, updateTime: String
    }

    // MARK: - Fields
    struct Fields: Codable {
        let unit, instructions: Business
        let longitude, latitude: Location
        let postalCode, street, brandName, state: Business
        let city, country: Business
        
    }

    // MARK: - Business
    struct Business: Codable {
        let stringValue: String
        
    //    private enum CodingKeys: String, CodingKey {
    //        case stringValue = "Business"
    //    }
    }

    // MARK: - Location
    struct Location: Codable {
        let doubleValue: Double
    }


    let url = "https://firestore.googleapis.com/v1/projects/goe-dash-llc/databases/(default)/documents/users/trCln2nBPi1oMLBjMJ2Z/pickup"

    URLSession.shared.dataTask(with: URL(string: url)!) { (data, _, error) in
        
        guard let data = data, error == nil else {
            fatalError("No data found")
        }

        let response = try? JSONDecoder().decode(DeliveryOffer.self, from: data)
        if let response = response {
            
            brandName.text =  Business(stringValue: brandName)
            
        print(response)
        } else if response == nil {
            print(error!.localizedDescription)
        }
        
    }.resume()
}

Really I am trying to understand it. I just found the perfect tutorial that. I’ll I update you once I figure it out.

I’d suggest making a separate post for this @coder3000

Also part of this is you need to update the UI on the main thread, not a background thread

Yes, I have done that. I have the connection to the UILabel now. But now I just need it to output the data for brandName to the UILabel.


import UIKit

class ViewController: UIViewController {
    
    @IBOutlet var brandName: UILabel!
//    @IBOutlet var unit: UILabel!
//    @IBOutlet var street: UILabel!
//    @IBOutlet var city: UILabel!
//    @IBOutlet var state: UILabel!
//    @IBOutlet var postalCode: UILabel!
//    @IBOutlet var country: UILabel!
//    @IBOutlet var latitude: UILabel!
//    @IBOutlet var longitude: UILabel!
//    @IBOutlet var instructions: UILabel!

    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        fetchData(brandName: brandName)
        
    }


}

func fetchData(brandName: UILabel!) {
    
    // MARK: - Welcome
    struct DeliveryOffer: Codable {
        let offers: [Document]
        
        private enum CodingKeys: String, CodingKey {
            case offers = "documents"
        }
    }

    // MARK: - Document
    struct Document: Codable {
        //let name: String
        let fields: Fields
        //let createTime, updateTime: String
    }

    // MARK: - Fields
    struct Fields: Codable {
        let unit, instructions: Business
        let longitude, latitude: Location
        let postalCode, street, brandName, state: Business
        let city, country: Business
        
    }

    // MARK: - BrandName
    struct Business: Codable {
        let stringValue: String
        
    //    private enum CodingKeys: String, CodingKey {
    //        case stringValue = "Business"
    //    }
    }

    // MARK: - Itude
    struct Location: Codable {
        let doubleValue: Double
    }

    let url =  "https://firestore.googleapis.com/v1/projects/goe-dash-llc/databases/(default)/documents/users/trCln2nBPi1oMLBjMJ2Z/pickup"
    

    URLSession.shared.dataTask(with: URL(string: url)!) { (data, _, error) in
        
        guard let data = data, error == nil else {
            fatalError("No data found")
        }

        let response = try? JSONDecoder().decode(DeliveryOffer.self, from: data)
        if let response = response {
            
            DispatchQueue.main.async {

                brandName.text =  "Hello World!"

            }
            
        print(response)
            
        } else if response == nil {
            print(error!.localizedDescription)
        }
        
    }.resume()
    
    func setDisplay(display: Fields) {
        
        
        
    }
}

I’m having issues outputting to my TableView. This will help with all my other networking issues. Your help would be much appreciated! Here is the code:

import UIKit

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource{
    
    
    var deliveryOffer: DeliveryOffer?
    
    private var tableView: UITableView = {
        let table = UITableView(frame: .zero, style: .grouped)
        table.register(UITableView.self, forCellReuseIdentifier: "cell")
        return table
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
       parseJSON()
        view.addSubview(tableView)
        tableView.delegate = self
        tableView.dataSource = self

    }
    
    // TableView
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        <#code#>
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        <#code#>
    }
    
    
    
   // JSON
    private func parseJSON() {
        // Make a path to the data.json file.
        guard let path = Bundle.main.path(forResource: "data", ofType: "json") else {
            return } // forResource: is the file name, ofType: is the file extention
            // Pass in the path which is a string
            let url = URL(fileURLWithPath: path)
//            var deliveryOffer: DeliveryOffer? // For testing
            // Access the content of the URL and it is throw, so put it in a do-catch method and try
            do {
                let jsonData = try Data(contentsOf: url)
                deliveryOffer = try JSONDecoder().decode(DeliveryOffer.self, from: jsonData)
                
                 // For testing
                if let deliveryOffer = deliveryOffer {
                    print(deliveryOffer)
                }
                else {
                    print("Failed to parse")
                }
                return
            }
            catch {
                print("Error: \(error)")
            }
    }

}

// MARK: - DeliveryOffer
struct DeliveryOffer: Codable {
    let fields: Fields
}

// MARK: - Fields
struct Fields: Codable {
    let brandName, unit, street, postalCode, city, state, country, instructions: BusinessInfo
    
}

// MARK: - BrandName
struct BusinessInfo: Codable {
    let stringValue: String
}

Here is what JSON:

{
    
    "fields": {
        "brandName": {
            "stringValue": "brandName"
        },
        "unit": {
            "stringValue": "3050"
        },
        "street": {
            "stringValue": "St. Daphne Dr."
        },
        "postalCode": {
            "stringValue": "63301"
        },
        "city": {
            "stringValue": "St. Charles"
        },
        "state": {
            "stringValue": "MO"
        },
        "country": {
            "stringValue": "USA"
        },
        "instructions": {
            "stringValue": "Hello! I am a text field!"
        }
    }
}

The thing that stumps me the most is, how I can use a JSON object the doesn’t use arrays [ ], and then display them into a table view. Doses’t the count look for an array setup? My client has this for his endpoint so I know I can’t change the JSON code but can something be done in the model to express and array of the “Fields” and still have the JSON network output the data? I need help with this.

Is this a way to concert a JSON object into an array for use in a tableView, using my above JSON code:


    var results = [Fields(brandName: BusinessInfo(stringValue: "brandName"), unit: BusinessInfo(stringValue: "unit"), street: BusinessInfo(stringValue: "street"), postalCode: BusinessInfo(stringValue: "postalCode"), city: BusinessInfo(stringValue: "city"), state: BusinessInfo(stringValue: "state"), country: BusinessInfo(stringValue: "country"), instructions: BusinessInfo(stringValue: "instructions"))]

,if so I get this error:

I think the main problem is that I have’t found away to conform to the delegate and dataSource for UITableView. I would appreciate the assistance.