-1

I'm trying to create a very simple todo list app in swift and when I call the reloadData method on my UITableView I get this error: "Unexpectedly found nil while implicitly unwrapping an Optional value". I'm calling this method when the user clicks an add button after typing something into a text field on a separate view controller from the tableView. The thing they type is supposed to get added to the table view, but it doesn't, and I just get an error.

I looked online and found people with similar problems but I couldn't figure out how to implement them into my code or didn't understand them as I am very new to swift. I also tried putting the text field on the same view controller as the table view and that fixed the problem, so I'm guessing it has something to do with that.

I have all my code in ViewController.swift. Here it is:

import UIKit

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
    
    @IBOutlet weak var tableView: UITableView!
    @IBOutlet weak var editButton: UIBarButtonItem!
    @IBOutlet weak var textField: UITextField!
    
    var tableViewData = ["Apple", "Banana", "Orange", "Peach", "Pear"]
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    // MARK: Tableview methods
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return tableViewData.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        cell.textLabel?.text = tableViewData[indexPath.row]
        return cell
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        // print(tableViewData[indexPath.row])
    }
    
    // Allows reordering of cells
    func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
        return true
    }
    
    // Handles reordering of cells
    func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
        let item = tableViewData[sourceIndexPath.row]
        
        tableViewData.remove(at: sourceIndexPath.row)
        tableViewData.insert(item, at: destinationIndexPath.row)
    }
    
    // Allow the user to delete cells
    func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
        if editingStyle == UITableViewCell.EditingStyle.delete {
            tableViewData.remove(at: indexPath.row)
            tableView.reloadData()
        }
    }
    
    // MARK: IBActions
    
    @IBAction func edit(_ sender: Any) {
        tableView.isEditing = tableView.isEditing
        
        switch tableView.isEditing {
        case true:
            editButton.title = "Done"
        case false:
            editButton.title = "Edit"
        }
    }
    
    @IBAction func add(_ sender: Any) {
        let item: String = textField.text!
        tableViewData.append(item)
        textField.text = ""
        tableView.reloadData() // <------ **This line gives me the error**
    }
    
}

Also, I tried optional chaining on the line that gave me an error by writing, tableView?.reloadData(). It makes the error go away, but none of the items get added to the table view.

Not sure if it's necessary, but here is an image of the storyboard so you can see all the screens enter image description here

Sorry if this is a really obvious problem. Like I said I'm very new to swift and iOS applications in general.

Thanks in advance!

SteveFerg
  • 3,466
  • 7
  • 19
  • 31
Zemelware
  • 93
  • 8

1 Answers1

1

It looks like you are assigning ViewController class to both your first controller (which holds the table view) AND to your second controller (with the text field).

That's not going to work.

Add this class to your project, assign it as the "New Item" view controller's Custom Class, and connect the @IBOutlet and @IBAction:

class NewItemViewController: UIViewController {

    // callback closure to tell the VC holding the table view
    //  that the Add button was tapped, and to
    //  "send back" the new text
    var callback: ((String) -> ())?
    
    @IBOutlet weak var textField: UITextField!

    @IBAction func add(_ sender: Any) {
        let item: String = textField.text!
        callback?(item)
        textField.text = ""
    }
    
}

Next, change your ViewController class to the following:

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
    
    @IBOutlet weak var tableView: UITableView!
    @IBOutlet weak var editButton: UIBarButtonItem!
    
    var tableViewData = ["Apple", "Banana", "Orange", "Peach", "Pear"]
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // if you're not already seeing "Apple", "Banana", "Orange", "Peach", "Pear"
        // add these two lines
        //tableView.dataSource = self
        //tableView.delegate = self
    }
    
    // MARK: Tableview methods
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return tableViewData.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        cell.textLabel?.text = tableViewData[indexPath.row]
        return cell
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        // print(tableViewData[indexPath.row])
    }
    
    // Allows reordering of cells
    func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
        return true
    }
    
    // Handles reordering of cells
    func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
        let item = tableViewData[sourceIndexPath.row]
        
        tableViewData.remove(at: sourceIndexPath.row)
        tableViewData.insert(item, at: destinationIndexPath.row)
    }
    
    // Allow the user to delete cells
    func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
        if editingStyle == UITableViewCell.EditingStyle.delete {
            tableViewData.remove(at: indexPath.row)
            tableView.reloadData()
        }
    }
    
    // MARK: IBActions
    
    @IBAction func edit(_ sender: Any) {
        tableView.isEditing = !tableView.isEditing
        
        switch tableView.isEditing {
        case true:
            editButton.title = "Done"
        case false:
            editButton.title = "Edit"
        }
    }

    // when "New Item" button is tapped, it will segue to
    // NewItemViewController... set the callback closure here
    
    // prepare for segue is called when you have created a segue to another view controller
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        
        // error checking is always a good idea
        //  this properly unwraps the destination controller and confirms it's
        //  an instance of NewItemViewController
        if let vc = segue.destination as? NewItemViewController {
            // callback is a property we added to NewItemViewController
            //  we declared it to return a String
            vc.callback = { item in
                self.tableViewData.append(item)
                self.tableView.reloadData()
                self.navigationController?.popViewController(animated: true)
            }
        }
    }
    
}

When you tap the "Add Item" button, we're assuming you have that connected to segue to the "New Item" view controller. By implementing:

override func prepare(for segue: UIStoryboardSegue, sender: Any?)

we will get a reference to the "New Item" view controller that is about to appear, and we'll assign it a "callback closure".

When we type some text and tap the "Add" button in the next controller, it will "call back" to the first controller, passing the newly typed text. That is where we'll update the data array, reload the table, and pop back on the navigation stack.

DonMag
  • 69,424
  • 5
  • 50
  • 86
  • Thanks so much for your answer! I'm just trying to understand what's going on in the code. I did some research on closures, so I get it a little more, but I still need a bit more explaining. Correct me if I'm wrong, but are we creating a closure that's being called when the add button is clicked, and we are passing in the item into the closure? And then in ViewController.swift right before we click "Add Item", we store a reference to the NewItemViewController and we create the closure with "item" as a parameter, and that's the code that gets executed when we call the closure? – Zemelware Aug 07 '20 at 04:49
  • I have other questions that I couldn't ask due to the limited comment space. If you are able to, I would appreciate if you could answer them (it would help a lot). They are commented in the code in this image: https://imgur.com/a/F4aKPWS – Zemelware Aug 07 '20 at 05:03
  • @Zemelware - I edited my answer and added a few additional comments in the code to try and answer your questions. It sounds like you're getting a general understanding of what's going on. Using a closure in this manner is a very common way to communicate between controllers. You could also use the `protocol / delegate` pattern, but Swift seems to favor closures. There would be very little difference anyway, but as you continue to learn you'll come across protocols and delegates soon enough. – DonMag Aug 07 '20 at 12:37
  • Thanks, this really helps! I appreciate it! Also, aren't UITableViewDataSource and UITableViewDelegate already protocols? – Zemelware Aug 07 '20 at 15:31
  • Hi @DonMag, I am wondering would you be able to help with a ARkit/Scenekit related problem? Best regards. – swiftlearneer Aug 08 '20 at 14:44