I am following a tutorial for a simple list app, where you add items to a list via UITextField. However, it crashes at tableView.reloadData() and I don't know why. Making tableView optional causes it to not crash, but it also causes the app to not add the item to the list. Here is the class code:
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, UITextFieldDelegate {
struct todo {
var text: String
var isDone: Bool
}
var todos = [todo]()
#IBOutlet var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
todos.append(todo(text: "test", isDone: false))
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return todos.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "todo-cell", for: indexPath)
let todo = todos[indexPath.row]
cell.textLabel?.text = todo.text
if todo.isDone {
cell.accessoryType = .checkmark
} else {
cell.accessoryType = .none
}
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
var todo = todos[indexPath.row]
todo.isDone = !todo.isDone
todos[indexPath.row] = todo
tableView.reloadRows(at: [indexPath], with: UITableView.RowAnimation.automatic)
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
todos.append(todo(text: textField.text!, isDone: false))
tableView.reloadData() // Thread 1: Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value... crashes here!!
textField.text = ""
textField.resignFirstResponder()
return true
}
}
My first guess is that you have not linked the tableview in your storyboard to your view controller.
Maybe you should check this first. Put a break point in viewDidLoad to see if your tableview has been set. If its nil, then there is your problem.
tableView object is nil.
#IBOutlet var tableView: UITableView! // is not link with viewController
Related
I have written code for performing Delete,Insert,Update . when I execute my code I'm getting an Error like "Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid index path for use with UITableView. Index paths passed to table view must contain exactly two indices specifying the section and row. Please use the category on NSIndexPath in NSIndexPath+UIKitAdditions.h if possible."
I'm adding the code here
import UIKit
class ViewController: UIViewController {
#IBOutlet var tableView: UITableView!
#IBOutlet var Insert: UIButton!
#IBOutlet var txtfield: UITextField!
var index = IndexPath()
var models = ["1.Audi","2.Hyundai","3.Bentley","4.Chevrolet","5.Dodge","6.Electric","7.Ford","8.Genesis","9.Honda","10.Ferrari","11.Nissan","12.Porche","13.Range Rover","14.Lamborgini","15.McLaren","16.koneisegg","17.Volvo","18.Mazda","19.Bmw"]
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
}
#IBAction func textFieldEdit(_ sender: UITextField) {
//self.tableView.reloadData()
if let cell = tableView.cellForRow(at: index) as? UITableViewCell{
cell.textLabel?.text = self.txtfield.text
}
}
#IBAction func insertBtn(_ sender: UIButton) {
if let txt = txtfield.text, !txt.isEmpty{
//self.models.append(txt)
self.models.insert(txt,at: 0)
tableView.beginUpdates()
tableView.insertRows(at: [IndexPath(row: 0, section: 0)], with: .fade)
tableView.endUpdates()
print (models.count)
}
}
}
extension ViewController: UITableViewDataSource,UITableViewDelegate{
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return models.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = models[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
return .delete
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete{
tableView.beginUpdates()
models.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .fade)
tableView.endUpdates()
}
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
index = indexPath
self.txtfield.text = models[indexPath.row]
}
}
ScreenShot of My storyBoard
You didn't explain when you're getting the error.
Running your code as-is, no apparent errors.
However, my guess is that you're hitting an issue when you haven't yet selected a row, or when you've done something to change the selection while editing in the text field.
Try changing your textFieldEdit func to this:
#IBAction func textFieldEdit(_ sender: UITextField) {
// make sure
// a row is selected
// and
// we can get a reference to that cell
guard let pth = tableView.indexPathForSelectedRow,
let cell = tableView.cellForRow(at: pth)
else {
return
}
// properly unwrap optional .text property
let str = sender.text ?? ""
// update the data
self.models[pth.row] = str
// update the cell
cell.textLabel?.text = str
}
You've instantiated an IndexPath without properties
var index = IndexPath()
If you refer to it before setting a valid property you'll get that error.
Possible Solution
var index: IndexPath?
Use an optional for your index rather than setting an invalid default property. It'll be nil until you assign an instance.
You'll then have to use index?. or if let index = index when using the property, but it'll be safe.
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.
I am working in swift. I have a textfield that is in a tableview cell. I am trying to store the text of each text field in the tableview so they when the user adds or deletes a row, and then the tableview reloads data, that the text fields stay filled in with the appropriate data.
I tried adding a textfielddidendeditting function but for some reason it is not being called.
EDIT:
Here is my code:
tableViewController:
import UIKit
var rowCount = 0
var textArray = [String]()
class TableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return rowCount
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! TableViewCell
return cell
}
#IBAction func addRow(_ sender: Any) {
rowCount = rowCount + 1
textArray.append("")
tableView.reloadData()
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
rowCount = rowCount - 1
textArray.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .fade)
} else if editingStyle == .insert {
}
}
}
tableViewCell:
import UIKit
class TableViewCell: UITableViewCell, UITextFieldDelegate {
#IBOutlet weak var textField: UITextField!
override func awakeFromNib() {
super.awakeFromNib()
textField.delegate = self
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
if let myIndexPath = tableView.indexPathForSelectedRow {
let newText = textField.text
textArray.insert(newText, at: myIndexPath)
}
return true
}
}
As far as I could suggest (without any code insight given) you could do the following:
Use a callback in your cell, which gets called every time the textfield ends editing:
.
class MyTableViewCell: UITableViewCell {
var textHasChanged: ((String) -> Void)?
...
}
extension MyTableViewCell: UITextFieldDelegate {
// this gets called every time the user ends editing the text field
func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
let newValue = textField.text //here you have your value
// now save it to your data source
self.textHasChanged(newValue)
}
}
In a initializer or in awakeFromNib() function (depends on your usage), set the .delegate property of the textfield to self
Now, to have each cell display the value from the datasource and to apply the changed text to your tableView datasource, add the following lines to your UITableViewController:
.
class MyTableViewController: UITableViewController {
var textArray: [String] = ["abc", "def"]
...
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "myCell", for: indexPath)
cell.textField.text = textArray[indexPath.row]
cell.textHasChanged = { (newValue) in
self.textArray[indexPath.row] = newValue
}
return cell
}
Just comment if you have further questions
I've been trying for ages to get the user's input to save and add to the table so it can be used as a to do list but I am unable to. I've tried searching on here but nothing has helped me. Here is my whole code. This is actually my first time using Xcode and Swift so there may be some obvious mistake that I've missed. The problem is in the beginning of the textFieldShouldReturn function (sorry I haven't explained this well!)
import UIKit
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, UITextFieldDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return todos.count
}
struct todo {
var text: String
var isDone: Bool
}
// this is the problem area:
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
let t = todo(text: textField.text!, isDone: false)
todos.append(t)
tableView.reloadData()
textField.text = ""
textField.resignFirstResponder()
return true
}
var todos = [todo]()
#IBOutlet weak var tableView: UITableView!
// the rest
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
var todo = todos[indexPath.row]
todo.isDone = !todo.isDone
todos[indexPath.row] = todo
tableView.reloadRows(at: [indexPath], with: UITableViewRowAnimation.automatic)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let id = "todo-cell"
let cell = tableView.dequeueReusableCell(withIdentifier: id, for: indexPath)
let todo = todos[indexPath.row]
cell.textLabel?.text = todo.text
if todo.isDone {
cell.accessoryType = .checkmark
}else{
cell.accessoryType = .none
}
return cell
}
override func viewDidLoad() {
super.viewDidLoad()
let t = todo (text:"clean my room", isDone: false)
todos.append(t)
}
}
Looking at your code, assuming that's all of it, you're not assigning the delegates anywhere. In viewDidLoad() you have to set your UITableViewDelegate, UITableViewDataSource and UITextFieldDelegate.
tableView.delegate = self
tabelView.dataSource = self
textField.delegate = self
I am using two view Controllers i.e. StateListVC and PlayVC. In StateListVC, I am using container view with 3 VCs. After tapping on 3rd View Controller, I am going to PlayVC with delegate method. I don't know how to do that. Please help me to resolve this issue.
StateListVC
class StateListVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
if isMoveToAnotherVC
{
let vc = self.storyboard?.instantiateViewController(withIdentifier: "PlaceVC") as! PlaceVC
vc.delegate = self
}
}
}
extension StateListVC: MoveToAnotherVC {
func moving(string: String) {
print(string)
}
}
ThirdVC
protocol MoveToAnotherVC {
func moving(string: String)
}
class PlaceVC: UIViewController {
#IBOutlet weak var tableViewPlace: UITableView!
var delegate: MoveToAnotherVC? = nil
var arrPlace = ["Place1", "Place2"]
override func viewDidLoad() {
super.viewDidLoad()
}
extension PlaceVC: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return arrPlace.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.text = arrPlace[indexPath.row]
return cell
}
}
extension PlaceVC: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
isMoveToAnotherVC = true
print(delegate)
guard let delegate = self.delegate else{
print("Delegate not set")
return
}
delegate.moving(string: "test")
}
}
This is hooked up fine:
protocol MoveToAnotherVC: AnyObject {
func moving(string: String)
}
class StateListVC: UIViewController, MoveToAnotherVC {
override func viewDidLoad() {
super.viewDidLoad()
let vc = self.storyboard?.instantiateViewController(withIdentifier: "PlaceVC") as! PlaceVC
vc.delegate = self
}
func moving(string: String) {
print(string)
}
}
class PlaceVC: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet weak var tableViewPlace: UITableView!
weak var delegate: MoveToAnotherVC?
var arrPlace = ["Place1", "Place2"]
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return arrPlace.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = arrPlace[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
delegate?.moving(string: "test")
}
}
I made the protocol a class protocol, moved the view controller instantiation into viewDidLoad, removed some (what I consider) extraneous unwrapping, and stripped your protocol/delegate pattern down to the basics. This works. I would plug this into your project and add what you need piece by piece until it breaks because your problem is outside of this. I suspect it may have to do with something you didn't include in your question but this answers your question about how to set up a protocol/delegate pattern.