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.
Related
I'm trying to create an App but I'm struggling with a simple problem: How can I add a delete button to my custom cells?
I've created a NewCocoaTouchClass
I've created my custom design (added also a button for every cell) and provided the Outlets in the TableViewCellController
I managed to implement my custom cell to another viewController
I created a button on my viewController
I'd like that when the user taps on my new button, cell buttons will appear, and then I can delete cells singularly. When I create the IbAction I can't retrieve the cell code because the cell is defined only in my tableViewCode.
Thank in advance
Ps: I also implemented deleting cells by swipe but my custom cell design is incompatible with the standard rectangular delete option (this is awful).
TableViewCellController
import UIKit
class CustomTableTableViewCell: UITableViewCell {
static let identificatore = "CustomTableTableViewCell"
static func nib() -> UINib {
return UINib(nibName: "CustomTableTableViewCell", bundle: nil)
}
#IBOutlet weak var ImmagineCella: UIImageView!
#IBOutlet weak var TestoCella: UILabel!
#IBOutlet weak var Button: UIButton!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
ViewController
import UIKit
class MaterieViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var materie : [String] = ["zoifvdhfdv", "szvzv", "zdvzfv", "zfdvbfdb", "bfzdfb"]
#IBOutlet weak var TableViewMaterie: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
TableViewMaterie.register(CustomTableTableViewCell.nib(), forCellReuseIdentifier: CustomTableTableViewCell.identificatore)
TableViewMaterie.delegate = self
TableViewMaterie.dataSource = self
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return materie.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: CustomTableTableViewCell.identificatore, for: indexPath) as! CustomTableTableViewCell
cell.TestoCella.text = materie[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 67.83
}
#IBAction func EditButton(_ sender: UIButton) {
}
}
This is the code I use in my project
func tableView(_ tableView: UITableView, titleForDeleteConfirmationButtonForRowAt indexPath: IndexPath) -> String?
{
return "Delete"
}
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if (editingStyle == .delete) {
//Add your code
}
}
You can write with add target
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: CustomTableTableViewCell.identificatore, for: indexPath) as! CustomTableTableViewCell
cell.TestoCella.text = materie[indexPath.row]
cell. Button.tag = indexPath.row
cell. Button.addTarget(self, action: #selector(buttonClicked(sender:)), for: .touchUpInside)
return cell
}
func buttonClicked(sender: UIButton) {
let buttonRow = sender.tag
print(buttonRow)
}
I'm trying to add a cell to a tableView that's in viewController by sending data via a segue from another viewController.
class FavoritesViewController: UIViewController {
var shops = [
"hello world",
"hello world",
"hello world"
]
#IBOutlet weak var table: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
table.delegate = self
table.dataSource = self
table.reloadData()
}
}
//protocol FavoritesDelegate: class {
// func add(_ shopName: String)
//}
extension FavoritesViewController: UITableViewDelegate, UITableViewDataSource{
func add(_ shopName: String) {
print(shopName)
shops.append(shopName)
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return shops.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell",for: indexPath)
cell.textLabel?.text = shops[indexPath.row]
return cell
}
// define the action. In this case "delete"
func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
return .delete
}
// do the actual deleting
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
tableView.beginUpdates()
shops.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .fade)
tableView.endUpdates()
}
}
}
And here's the function call in the other viewController (prepare):
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
favoritesDestinationVC = segue.destination as! FavoritesViewController
favoritesDestinationVC.add(shopName!)
}
I know what's causing the error (favoritesDestinationVC creates a new instance where tableView is nil), but I don't know how to solve it. Any ideas on how I could add an entry to the tableView that way (and updating the table afterwards) without my app crashing?
Make your shops var public and then in the segue prepare callback use it directly. Check a variable first whether it has a valid value other than the nil, if it does then proceed to avoid crashes. You can do it using an if statement to verify as you can see in the sample code. Or you can use optionals to avoid crashes. See the code below.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.destination is FavoritesViewController {
let destinationVC = segue.destination as? FavoritesViewController // Use optionals which has ? (question mark)
// To avoid crashes check if shopName has a valid value other than nil
if let newShopName = shopName {
// It is possible that the "shopName" has a nil value when the program reaches here.
// That's why we will use the verified "newShopName" instead of "shopName" itself to avoid any crash.
destinationVC?.shops.append(newShopName) // What I recommend, if not convenient for you just delete this line,
// destinationVC?.add(newShopName) // after deleting the recommended line, uncomment this line for your convenience.
}
}
}
i'm trying to make a ordering food app for restaurants, when i add items to the cart for the first time table view loads the singleton array i've made. but when i go back to the menu and choose another item the array is updated but the table view of the cart doesn't, i'm using tabbarcontroller. tried to use tableview.reloadData() in different places still new data added to the array doesn't appear
class CartVC: UIViewController, UITableViewDelegate, UITableViewDataSource{
#IBOutlet weak var priceLbl: UILabel!
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableView.reloadData()
// Do any additional setup after loading the view.
tableView.delegate = self
tableView.dataSource = self
tableView.reloadData()
updateView()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return DataService.instance.cartItems.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier: "cartCell") as? CartCell{
let item = DataService.instance.cartItems[indexPath.row]
cell.configCell(cart: item)
return cell
} else {
return UITableViewCell()
}
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
DataService.instance.cartItems.remove(at: indexPath.row)
tableView.beginUpdates()
tableView.deleteRows(at: [indexPath], with: .automatic)
updateView()
tableView.endUpdates()
}
}
func updateView(){
var sum = 0
for item in DataService.instance.cartItems{
sum += item.itemPrice * item.quantity
print(item.itemPrice)
}
priceLbl.text = "$\(sum)"
}
#IBAction func orderPressed(_ sender: Any) {
// upload order to firebase, clear cart and move to orderVC
}
}
In tabbar every time viewDidLoad not called so you need to reload the data in viewWillAppear or viewDidAppear. Here is the reference.
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
tableView.delegate = self
tableView.dataSource = self
updateView()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
tableView.reloadData()
}
UITableViewDelegate is not reactive. You have to call reloadData() when you want to update the tableView content (when your DataService.instance value is updated).
In a MVP world, your "add" action will trigger an event to the presenter. And the presenter sends back to the view an action to update the tableView, after updating your DataService.
Maybe you should look at RxSwift/ReactiveCocoa, which provides APIs to automatically bind your DataService.instance array to the cells rendered in the UITableView.
How to make Each Cell open the specific view for its indexpath.
In tableview didselect what should i do to make each cell open as its own indexpath so each cell have a different data in the next view
i am tryin when click in a cell in the tableview the next view present it self with it's own data as the next view contain a uitextview like in note app
what should i apply at row didselect
// MARK: -TableFunctions
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return SavingTasks.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let newtask = self.SavingTasks[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! TableViewCell
cell.TheLabel?.text = newtask
return cell
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if(editingStyle == .delete)
{
self.SavingTasks.remove(at: indexPath.row)
self.TasksTable.deleteRows(at: [indexPath], with: .fade)
GetData()
}
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let NewIndex = self.SavingTasks[indexPath.row]
let view = self.storyboard?.instantiateViewController(withIdentifier: "TaskDetail") as! TaskDetail
view.SavingDetails = [NewIndex]
view.Index = indexPath.row
self.navigationController?.pushViewController(view, animated: true)
}
next class should be appeared
class TaskDetail: UIViewController {
var Delegate: NoteDetailDelegate!
var SavingDetails = [String]()
var Index: Int?
#IBOutlet weak var TaskDetailsFiled: UITextView!
#IBAction func SaveTDF(_ sender: UIButton) {
UserDefaults.standard.set(TaskDetailsFiled.text, forKey: "Saved")
self.navigationController?.popViewController(animated: true)
}
You can use a segue and prepare(for:sender:) to get the next view controller ready more easily than instantiating the view controller and popping it via code. Official documentation here and a sample app from Apple here
An implementation example:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "mysegue"{
if let nextViewController = segue.destination as? NextViewController{
nextViewController.Index = 2
}
}
}
A highlight from the official doc:
For example, if the segue originated from a table view, the sender parameter would identify the table view cell that the user tapped
If you want to stick with the code implementation, you can call view.myvariable = myvalue in your didSelect
How to change tableView textLabel when different buttons is clicked. I have two action buttons brandButton and profileButton, what I want to happen is when I click brandButton brandInformation will show up to the tableView textLabel, and when I click profileButton profileInformation will show up.
import UIKit
class StreamDetailController: UITableViewDataSource, UITableViewDelegate{
#IBOutlet weak var tableViewController: UITableView!
var brandInformation = ["Hat:","Top:","Pants:","Shoes"]
var profileInformation = ["tim", "master"]
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return brandInformation.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableViewController.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.text = brandInformation[indexPath.row]
return cell
#IBAction func brandButton(_ sender: Any) {
}
#IBAction func profileButton(_ sender: Any) {
}
Note:
in this line:
#IBOutlet weak var tableViewController: UITableView!
the variable is wrong and can be confusing. the tableViewController should be renamed to tableView
1. Solution
add a class var to hold which view you like to see and chenge it with the buttons. then call reloadData on the table to refresh the content:
cell.textLabel?.text = brandInformation[indexPath.row]
to
var isShowBrand = true
// [...]
if isShowBrand {
cell.textLabel?.text = brandInformation[indexPath.row]
} else {
cell.textLabel?.text = profileInformation[indexPath.row]
}
and also for the rowcount (you can also use the ternary operator:
return isShowBrand ? brandInformation.count : profileInformation.count
(if you like to save it per cell then you need to save this info similar how you save the cell data var showBrand = [true, false] - but check that all 3 arrays have the same count of items to avoid index out of bounds errors)
2. Solution
just make a additional array tableData and you set in the buttons the array you need to see. and all data populating is done with the tableData array (you need to change all brandInformation to tableData)
var tableData = [String]()
// [...]
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tableData.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableViewController.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.text = tableData[indexPath.row]
return cell
}
// [...]
#IBAction func brandButton(_ sender: Any){
tableData = brandInformation
tableViewController.reloadData()
}
#IBAction func profileButton(_ sender: Any){
tableData = profileInformation
tableViewController.reloadData()
}