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
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!
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.