Incorrect segue behavior?

I have a split view app the works but I have a bug in it than I haven't been able to fix. I embeded the detail view controller in a navigation controller. The cell and the add button are connected to the embedded navigation controller. If I click on a cell to edit a record and then modally add a new record rather than showing the new record when I unwind the segue it remains on the edited record. Is there something I overlooked in the IB or a function I should add?

Accepted Reply

You are right it doesn't answer the question.


If you want to use the ToDo List app from Apple's "App Developement With Swift" on an iPad split view these two lines of code are one solution to make it work correctly.


Add this to the "if let SelectedIndexPath" code when you are editing a record to force it to reselect the row:

tableView.selectRow(at: selectedIndexPath, animated: true, scrollPosition: .none)


Add this to the modal segue when you add a record so no row is selected:

if let index = self.tableView.indexPathForSelectedRow {

self.tableView.deselectRow(at: index, animated: true)

}

Replies

did you check which is the selected cell ?


you could try to select the created cell programmatically before unwinding.


find the indexPath

let numberOfRows = // probably the last one ? Or get the row of the created cell
let indexPath = IndexPath(row: numberOfRows, section: 0)

Then

myTableView.selectRow(at: indexPath, animated: true, scrollPosition: .bottom)
myTableView.(myTableView, didSelectRowAt: indexPath)

Which problem do you get on iPad ? Can you show the code where error occurs ?


The concept of delegate is in fact simple here.


The tableView object may use functions (like tableView(didSelect) );

These functions are defined in the tableView class protocol (UITableViewDelegate or UITableViewDataSource)

They are implemented in another class, the delegate (here an UIViewController): when needed, those methods are called by the tableview.

That's why you have to declare the delegate = self in the viewDidload.


Note: if the parent class UITableViewController, all this is done automatically.


Note: this thread if for Swift3, but may be of interest for you

h ttps://stackoverflow.com/questions/31247991/how-to-programmatically-select-a-row-in-uitableview-in-swift-1-2-xcode-6-4

You didn't answer my question.


You sayed it works on iPhone. True ?

Not on iPad. True ?


So, what is the problem on iPad ?


Can you also explain in your code:


override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        super.prepare(for: segue, sender: sender)
       
        switch (segue.identifier ?? "") {
           
        case "addDetail":
            if let index = self.tableView.indexPathForSelectedRow {
                self.tableView.deselectRow(at: index, animated: true)
            }
            os_log("Adding New Aircraft.", log: OSLog.default, type: .debug)
           
        case "showDetail":
            if let indexPath = tableView.indexPathForSelectedRow {
                 let controller = (segue.destination as! UINavigationController).topViewController as! DetailViewController
                 let selectedDeadStick = deadsticks[indexPath.row]
                 controller.deadstick = selectedDeadStick
                 controller.navigationItem.leftBarButtonItem = nil
            
           
        default:
            fatalError("Unexpected Segue Identifier; \(String(describing: segue.identifier))")
        }
    }


addDetail segue is leading to what ?


Where do you create a new record ?


On line 12: where does showDetail lead to ? If no cell is selected, you do not prepare anything. Is that normal ?

The point is likely that split views do not work the same on iphone and iPad.


Is is well explained in Stanford course as I described in another post.


When yopu say doesn't work, does it crash ?


Could you modify for testing :


    override func prepare(for segue: UIStoryboardSegue, sender:
        Any?) {
        super.prepare(for: segue, sender: sender)
        guard segue.identifier == "saveUnwind" else { print("Not the good identifier", segue.identifier) ; return }



Instriment as well master view controller


   @IBAction func unwindShowDetailSegue(segue: UIStoryboardSegue) {
        guard segue.identifier == "saveUnwind" else { print("Not the good identifier", segue.identifier) ; return }
        let sourceViewController = segue.source as! DetailViewController     // I suspect the problem here
        if let deadstick = sourceViewController.deadstick {
            if let selectedIndexPath =
                tableView.indexPathForSelectedRow {
                deadsticks[selectedIndexPath.row] = deadstick
                tableView.reloadRows(at: [selectedIndexPath], with: .none)
                DeadStick.saveDeadSticks(deadsticks)
            } else {
                let newIndexPath = IndexPath(row: deadsticks.count,  section: 0)
                deadsticks.append(deadstick)
                tableView.insertRows(at: [newIndexPath], with: .automatic)
                DeadStick.saveDeadSticks(deadsticks)
          } else {
             print("No deadstick")
          }
        }


If you get the log No deadstick, then the problem is effectively because of the handling of splitviews.


Plesa tell, and we'll see how to modify

identifier is an optional. the == test fails.

In addition, why do you compare to "saveUnwind" and not to addDetail or showDetail ???

What are the identifiers you have defined for the two segues ?


I should try in both cases like this:


   @IBAction func unwindShowDetailSegue(segue: UIStoryboardSegue) {
        guard let id = segue.identifier else { print("Not the good identifier") ; return }     // id will be unwrapped
        if id  != "saveUnwind" { print("Not the good segue") ; return }


    override func prepare(for segue: UIStoryboardSegue, sender:  Any?) { 
        super.prepare(for: segue, sender: sender)
        guard let id = segue.identifier else { print("Not the good identifier") ; return }     // id will be unwrapped
        if id  !=  "saveUnwind" { print("Not the good identifier", segue.identifier) ; return }     // Is it the right name ?

What is not clear is:

- what are the segues you have defined ?

From which view to wich other view do they segue ?

What is their identifier ?


So, here, I do not understand what saveUnwind identifier refers to.


It would be clearer if you described :

View1 is …

View2 is …


segue addDetail goes from view 1 to view2

And so on.


The new code: where exactly do you get the error message ? Line 2 and 3 in both cases ?

It would be surprising as identifier is defined as:

var identifier: String?


Can you show the complete code, where the error shows ? Are you sure you have not written id!

For the last point, about id!


if you have :


   @IBAction func unwindShowDetailSegue(segue: UIStoryboardSegue) {
        guard let id = segue.identifier else { print("Not the good identifier") ; return }     // id will be unwrapped
        if id  != "saveUnwind" { print("Not the good segue") ; return }


Then, after line 2, id is not an optional.

So the test must be

if id  != "saveUnwind"

and NOT id!

if id!  != "saveUnwind"


That will correct the issue : "Cannot force unwrap value of non-optional type 'String' error."


The same here, as long as "saveUnwind" is the correct identifier, which I understand not (should be addDetail or editDetail depending on the segue)

    override func prepare(for segue: UIStoryboardSegue, sender:  Any?) {
        super.prepare(for: segue, sender: sender)
        guard let id = segue.identifier else { print("Not the good identifier") ; return }     // id will be unwrapped
        if id  !=  "saveUnwind" { print("Not the good identifier", segue.identifier) ; return }     // Is it the right name ?

So, you should change the test in the prepare(), to test the right identifier (as it seems you had ate beginning of this thread)

Probably you have defined only one unwind IBAction (unwindShowDetailWithSegue) in the originating view.

If you want a different unwind from edit, create another IBAction unwindShowAddlithSegue.


Then you will have a choice between 2 unwind.


This being said, I'm a bit lost in your app !

I looked at the example.


@IBAction func unwindToToDoList(segue: UIStoryboardSegue) {
    guard segue.identifier == "saveUnwind" else { return }
    let sourceViewController = segue.source as! ToDoViewController

    if let todo = sourceViewController.todo {
        if let selectedIndexPath = tableView.indexPathForSelectedRow {
            todos[selectedIndexPath.row] = todo
            tableView.reloadRows(at: [selectedIndexPath], with: .none)
        } else {
            let newIndexPath = IndexPath(row: todos.count, section: 0)
            todos.append(todo)
            tableView.insertRows(at: [newIndexPath], with: .automatic)
        }
    }
}

Of course yours is not exactly the same, but the logic should be the same:


   @IBAction func unwindShowDetailSegue(segue: UIStoryboardSegue) {
        guard segue.identifier == "saveUnwind" else { return }
        let sourceViewController = segue.source as! DetailViewController
  
        if let deadstick = sourceViewController.deadstick {
            if let selectedIndexPath = tableView.indexPathForSelectedRow {
                deadsticks[selectedIndexPath.row] = deadstick
                tableView.reloadRows(at: [selectedIndexPath], with: .none)
                DeadStick.saveDeadSticks(deadsticks)          // What is this doing ?
          
            } else {
                let newIndexPath = IndexPath(row: deadsticks.count,  section: 0)
                deadsticks.append(deadstick)
                tableView.insertRows(at: [newIndexPath], with: .automatic)
                DeadStick.saveDeadSticks(deadsticks)         // What is this doing ?
            }
        }

So, effectively, you have a unique IBAction unwindShowDetailSegue in the originating controller

But what is your saveDeadSticks doing ?

  if let deadstick = sourceViewController.deadstick {
            if let selectedIndexPath = tableView.indexPathForSelectedRow {
                deadsticks[selectedIndexPath.row] = deadstick
                tableView.reloadRows(at: [selectedIndexPath], with: .none)
                tableView.selectRow(at: selectedIndexPath, animated: true, scrollPosition: .none)
                DeadStick.saveDeadSticks(deadsticks)
              
                 } else {

                self.tableView.deselectRow(at: selectedIndexPath, animated: true)
                let newIndexPath = IndexPath(row: deadsticks.count, section: 0)
                deadsticks.append(deadstick)
                tableView.insertRows(at: [newIndexPath],with: .automatic)
                DeadStick.saveDeadSticks(deadsticks)
            }
        }
    }


Which line(s) exactly do you get the error ? 5 ? 10 ?


Can you show how you tried to solve the selectedIndexPath and how you tried to use it here ?


Just to see if mechanics work, try at line 5 and 10 to select or deselect row 0:

Error line 10 seem logic: there is no more row selected, hence selectedIndexPath is undefined (nil).


Your func


override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let selectedItem = indexPath
        print(selectedItem.row)
    }

is a good try, but you don't get selectedItem out of the func !


So,

- create a var at class level

var selectedItem = IndexPath(row: 0, section: 0)           // just to initialize


- change func as

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        selectedItem = indexPath
        // print(selectedItem.row)
    }


- change line 10 of previous post:

if let deadstick = sourceViewController.deadstick {
            if let selectedIndexPath = tableView.indexPathForSelectedRow {
                deadsticks[selectedIndexPath.row] = deadstick
                tableView.reloadRows(at: [selectedIndexPath], with: .none)
                tableView.selectRow(at: selectedIndexPath, animated: true, scrollPosition: .none)
                DeadStick.saveDeadSticks(deadsticks)
              
                 } else {

                self.tableView.deselectRow(at: selectedItem, animated: true)     // CHANGED HERE
                let newIndexPath = IndexPath(row: deadsticks.count, section: 0)
                deadsticks.append(deadstick)
                tableView.insertRows(at: [newIndexPath],with: .automatic)
                DeadStick.saveDeadSticks(deadsticks)
            }
        }
    }



Note : Line 10 is where I get the error. I tried 0 on line 10 but I get 'Cannot convert value of type 'Int' to expected argument type.


That's norma ; at asks for an IndexPath, not an Intl. What I proposed to try was:

self.tableView.deselectRow(at: IndexPath(row: 0, section: 0), animated: true)

Thanks. The code works but I had to add it when i segued because that part of the code doesn't execute unless you have deselected the row.


I deleted some of the converstation so this thread wouldn't be so long but I couldn't delete yours. I thought I would leave the answer in case someone else has this problem.

Ok, I will clean some parts of this thread. Even though I’m not sure that the answer you marked as correct is the most relevant.

You are right it doesn't answer the question.


If you want to use the ToDo List app from Apple's "App Developement With Swift" on an iPad split view these two lines of code are one solution to make it work correctly.


Add this to the "if let SelectedIndexPath" code when you are editing a record to force it to reselect the row:

tableView.selectRow(at: selectedIndexPath, animated: true, scrollPosition: .none)


Add this to the modal segue when you add a record so no row is selected:

if let index = self.tableView.indexPathForSelectedRow {

self.tableView.deselectRow(at: index, animated: true)

}