How to access variables in a viewcontroller for use in tableViewController using swift?

This is my first Swift/Firestore practice app and I am trying to gain access to the variables (i.e. eventLocation, eventDate) in the 'AddEventViewController' file to be used in 'EventTableViewController' file.

The variable will then be inserted at ?? to query a Firestore database

Firestore.firestore().collection("Locations").document(??).collection("Dates").document(??).collection("Events").getDocuments()

I have tried to most swift tutorials on the web but it has made me even more confused as I can't seem to figure out what I am missing or what I am doing wrong....

Would someone be kind enough to help a newbie to swift programming? Many thanks in advance!

EventTableViewController.swift

import UIKit
import FirebaseFirestore

class EventsTableViewController: UITableViewController {

 var eventsArray = [Event]()

 override func viewDidLoad() {
    super.viewDidLoad()
    loadData()
}

func loadData() {

   Firestore.firestore().collection("Locations").document(??).collection("Dates").document(??).collection("Events").getDocuments() { (querySnapshot, error) in
        if let error = error {
            print("Error getting documents: \(error)")
        } else {
            self.eventsArray = querySnapshot!.documents.compactMap({Event(dictionary: $0.data())})
            DispatchQueue.main.async {
                self.tableView.reloadData()
            }
        }
    }
}
override func numberOfSections(in tableView: UITableView) -> Int {
    // #warning Incomplete implementation, return the number of sections
    return 1
}

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    // #warning Incomplete implementation, return the number of rows
    return eventsArray.count
}


override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "eventCell", for: indexPath)
    
    let event = eventsArray[indexPath.row]
    
    // Configure the cell...
    cell.textLabel?.text = "\(event.programType)"
    cell.detailTextLabel?.text = "\(event.eventLocation) \(event.eventDate)"
    
    return cell
}

AddEventViewController.swift

import UIKit
import FirebaseFirestore

class AddEventViewController: UIViewController {

@IBOutlet weak var eventLocation: UITextField!
@IBOutlet weak var eventDate: UITextField!
@IBOutlet weak var programType: UITextField!

override func viewDidLoad() {
    super.viewDidLoad()
}

@IBAction func saveEventButtonTapped(_ sender: UIBarButtonItem) {
    guard let EventLocation = eventLocation.text, !EventLocation.isEmpty else { return }
    guard let EventDate = eventDate.text, !EventDate.isEmpty else { return }
    guard let Program = programType.text, !Program.isEmpty else {return}
    
    
    // Save profile details of person to Firestore as data fields
    let eventDetail: [String: Any] = ["Event Location": EventLocation, "Event Date": EventDate, "Program": Program]
   
    // Write data to Firestore
    Firestore.firestore().collection("Locations").document(EventLocation).collection("Dates").document(EventDate).collection("Events").document(Program).setData(eventDetail)
    { error in
        if let error = error {
            print("ERROR: Event not saved to database. Please try again! \(error)")
        } else {
            print("Event has been saved to the database!")
        }
        self.dismiss(animated: true, completion: nil)
    }
}

Event.swift

struct Event {
var programType: String
var eventLocation: String
var eventDate: String

var dictionary: [String: Any] {
    return [
        "programType": programType,
        "eventLocation" : eventLocation,
        "eventDate" : eventDate
    ]
}
}

extension Event : DocumentSerializable {
init?(dictionary: [String: Any]) {
    guard let programType = dictionary["Program"] as? String,
        let eventLocation = dictionary["Event Location"] as? String,
        let eventDate = dictionary["Event Date"] as? String else { return nil}
    
    self.init(programType: programType, eventLocation: eventLocation, eventDate: eventDate)
}
}

In order to access those variables, you need to have a variable of type AddEventViewController in your loadData() method.

Doing this depends mostly on how your app is set up, and how you transition from one view controller to another. From a quick look at your code, I assume the user is meant to input something into the text fields in the AddEventViewController, then perform some action that takes them to the EventTableViewController, is that correct?

The app is set up as 'Navigation Controller' --> 'EventTableViewController' --> 'Navigation Controller' --> 'AddEventViewController' using segues. Yes you are correct.

Thanks for taking a stab at it...

In that case (if I understand things correctly), there's a common way in iOS development to pass along the information using segues. People more experienced than me in iOS dev are welcome to correct me, but here goes:

  1. I'd create new variables in EventTableViewController to store the information you need. For instance:
class EventsTableViewController: UITableViewController {

    var eventsArray = [Event]()
    var myInformation: String? = nil

    // ...
}
  1. In your Storyboard (I'm assuming you're using storyboards here), select your segue, go to the inspector panel (on the right), and make up an identifier for it. It can be whatever you want, like "MySegueIdentifier1" for instance.

  2. In your AddEventViewController, implement a method called prepareForSegue:

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    // Here you should use the same identifier as in the Storyboard
    if (segue.identifier == "MySegueIdentifier1") {

        if let destinationViewController = segue.destinationViewController as? EventsTableViewController {

            // Here you set your new variable with any information you like
            destinationViewController.myInformation = eventLocation.text
        }
    }
}

Let me know if this works!

(I took the code for part 3 from this stack overflow question and adapted it)

The variables appear usable in the func loadData () method however fails to compile with the following errors

Value of optional type 'String?' must be unwrapped to a value of type 'String'

  • coalesce using ?? to provide a default when the optional value contains nil
  • force-unwrap using ! to abort execution i the optional value contains nil

Result: Using either option causes the app to crash upon loading the 'EventTableViewController' with the debugger saying myInformation = (String?) nil

If myInformation is nil then its value wasn't set in prepareForSegue(). Force-unwrapping an optional can be unsafe but using the nil coalescing operator should be safe so I don't see why it would cause a crash.

Hmm... I think you might be right.

It seems that the 'EventsTableViewController' crashes when running the simulator (using a nil coalescing operator for myInformation variable in the loadData(), thus preventing the 'AddEventViewController' from setting the value in the prepareForSegue().

Storyboard
storyboard

Oh, ok, I guess I understood things wrong.

Before we start, I’d like to point out that there’s probably no need for that second nagivation controller. I think it should be Navigation View Controller > Events Table View Controller > Add Event View Controller.

Now, can you tell me what you’re trying to accomplish? I’m assuming by looking at your storyboard that the user opens the event list, then taps the add button, fills out some information, than goes back to the list and expects the new event to show up there. Is that right?

No worries. My bad for not posting the storyboards earlier to aid understanding the question I was asking.

Thanks for the suggestion on the second navigation controller.

Yes you are correct. The use of the Firestore service was just for me to learn how the Swift language works in saving and retrieving data from a cloud database...

If you use only one navigation controller, it means that the Add Event View Controller will be pushed on the Events View Controller, which is not the common way to present an Edit screen. In other words, it is correct to have the second navigation controller for the edition if this one is presented modally on top of the other.

Now, back to the main issue: how to get access from (A) Events View Controller to data filled in (B) Add Event View Controller when this one is dismissed.

There are different ways to do it, but since you are using storyboards, I'd suggest to use an unwind segue, called when the user taps on Done in (B), and that goes to (A). You can then access the UIStoryboardSegue object passed to the unwind action (i.e. the called instance method) and retrieve properties defined in (B) such as eventLocation or eventDate.

To make things a bit cleaner and to avoid accessing UI elements from another view controller directly, you could make eventDetail as an optional instance property of (B), and set it in your save method. You could then access eventDetail from (A).

Also note that this question is not really Swift related, but rather how to use iOS API. You'll probably find better answers in Apple's forums.