Protocol question?

I am trying to understand when to use a delegate. When I add an item in my Master View controller I deselect all items. If, however, I cancel the add I need to reselect the last row I was on. Is that a case where I should set up a protocol in the detail view and delegate the reselection to the Master View Controller? Right now the cancel button is tied to the exit button on the detail view controller which returns it to the Master with no action taken.

Accepted Reply

I admire his dedication also. I didn't realize this would be such a complicated question.


I wonder if I should have stuck with my profession, accounting, rather than trying to create an app.


I found the solution to selecting the row. The code above selects the first row after deleting an item but clicking the done button is where the select row belongs to highlight the row.

override func setEditing(_ editing: Bool, animated: Bool) {

super.setEditing(editing, animated: animated)

if editing {

} else {

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

}

}

Replies

Yes, you can define Protocol for setting the selection (setSelection), have the master conform to this protocol, create a delegate property in Detail that is set to Master. Then call the delegate.setSelection in Detail.

You'll have also to transfer the selection from Master to a Detail property.


So, I would try to do this in a simple way :


- when you pass to the detail, pass the selectionPath(s) before unselecting all

- keep it in a property of the DetailView

var previousSelectionPaths …

- In the unwind from cancel, get those paths back from the unwindSegue.source

- Restore the selection

While I agree with Claude31 that a delegate protocol is a great way to handle this situation, I would do a couple of things differently to avoid unnecessary coupling between the two view controller implementations.


I would name the delegate method something like "newItemDidCancel" rather than "setSelection". The detail view controller should not have knowledge of what the parent view controller needs to do. Maybe you change from a table view to some custom view that doesn't have any concept of selection. Or maybe you just decide that you don't need to reselect anything on cancel, or select some different row(s) of your choosing. The point is, that decision is up to the parent table view controller. Changing the name of the method has literally no impact on the operation of the code, but I think it helps to define your interfaces properly when it comes to future maintenance and reuse. When you come back to that master VC code in a year or two, if you see a method named "setSelection" you have no idea if it's some internal method used to manipulate the table, or what. If you see "newItemDidCancel" then it's immediately obvious what it does.


Along the same lines, the detail view controller has no business knowing about selection paths or any other internal implementation details of the master. If you must store some data with the child (which I'm not sure is a good idea, unless you envision somehow having multiple child controllers active at some point) then it should just be an opaque blob of type Any with some neutral name that does not imply any particular data structures in use by the master, e.g. "parentData". Again, this reduces coupling and enables reuse.

Claude I tried your simple method rather than a delegate protocol and it worked fine. I found one other error in my code. When I delete an item I want to reselect a row. I tried adding this code when I delete a record but it doesn't work and I don't see why. What am I missing?


if editingStyle == .delete {

deadsticks.remove(at: indexPath.row)

tableView.deleteRows(at: [indexPath], with: .fade)

DeadStick.saveDeadSticks(deadsticks)

self.tableView.selectRow(at: indexPath, animated: true, scrollPosition: .top)

}

What do you mean "it doesn't work" ?

  • Does it crash ?
  • Does it select the wrong cell ?
  • Doesn't it select any cell ?


You have the indexPath pointing to a cell.

Then you remove this cell.

Which cell do you expect to select now ?


Anyway, and that's probably the cause of crash, if indexPath points to a now non existing row, you'll get a crash.


So, you could add a test :


if indexPath.row < tableView.numberOfRows(inSection: indexPath.section) {
      self.tableView.selectRow(at: indexPath, animated: true, scrollPosition: .top)
}


As for the use of the forum : it should be better to close the thread on the correct answer and open a new one, to keep thread from lasting for ever.

It doesn't select the cell.


I tried your test and also put a breakpoint before the editingStyle if statement. It doesn't even execute so I need to figure out what is going on there.


I'll close the thread from now on if I have a new issue. Hopefully I won't have many more questions. Thanks for the advice.

Doesn't select WHICH cell ? You just have deleted the selected cell !


I've tried this code on a table view, and it works OK.

I tried a breakpoint again and used your test code. It does skip the selectRow statement. According to Apple documentation the scroll position .top should cause it to scroll to the first record in the visible table view. This doesn't seem to be happening.

Can you show more code, to understand what you mean ?


Trying to interpret what happens without seeing code is a bit hard.


When you say: It does skip the selectRow statement, do you mean it skips line 2 below ?

If so, it is normal it does not scroll to top !


if indexPath.row < tableView.numberOfRows(inSection: indexPath.section) {
      self.tableView.selectRow(at: indexPath, animated: true, scrollPosition: .top)
}


if so, what are indexPath.row and tableView.numberOfRows(inSection: indexPath.section) values at this time ?


To get it scroll to the top in all cases, you could write:


if indexPath.row < tableView.numberOfRows(inSection: indexPath.section) {
      self.tableView.selectRow(at: indexPath, animated: true, scrollPosition: .top)
} else {
      self.tableView.selectRow(at: indexPath(row: 0, section: indexPath.section), animated: true, scrollPosition: .top)
}

I want it to scroll to the top so I used your code but get a "Cannot call value of non-function type 'IndexPath" error on the highlighted line:


override func tableView(_ tableView: UITableView, commit

editingStyle: UITableViewCellEditingStyle, forRowAt indexPath:

IndexPath) {

if editingStyle == .delete {

deadsticks.remove(at: indexPath.row)

tableView.deleteRows(at: [indexPath], with: .fade)

DeadStick.saveDeadSticks(deadsticks)

if indexPath.row < tableView.numberOfRows(inSection: indexPath.section) {

self.tableView.selectRow(at: indexPath, animated: true, scrollPosition: .top)

} else {

self.tableView.selectRow(at: indexPath(row: 0, section: indexPath.section), animated: true, scrollPosition: .top)

}

}

}

Normal, I mistyped indexPath, should be IndexPath starting with uppercase (name of type)


self.tableView.selectRow(at: IndexPath(row: 0, section: indexPath.section), animated: true, scrollPosition: .top)


You should also check there is at least one row in the section:

  if itableView.numberOfRows(inSection: indexPath.section)  > 0 {
     self.tableView.selectRow(at: IndexPath(row: 0, section: indexPath.section), animated: true, scrollPosition: .top)
}

Strange! It still doesn't work.


override func tableView(_ tableView: UITableView, commit

editingStyle: UITableViewCellEditingStyle, forRowAt indexPath:

IndexPath) {

if editingStyle == .delete {

deadsticks.remove(at: indexPath.row)

tableView.deleteRows(at: [indexPath], with: .fade)

DeadStick.saveDeadSticks(deadsticks)

if indexPath.row < tableView.numberOfRows(inSection: indexPath.section) {

self.tableView.selectRow(at: indexPath, animated: true, scrollPosition: .top)

} else {

if tableView.numberOfRows(inSection: indexPath.section) > 0 {

self.tableView.selectRow(at: IndexPath(row: 0, section: indexPath.section), animated: true, scrollPosition: .top)

}

}

}

}

When you say "doesn't work", please be more specific.


What happens exactly or what doesn't happen ?

In which circumstances ?


Do other tableView functions work properly (liking adding an entry in the table) ?

If no, check if you have properly set the dataSource and delegate for the tableView.


Could you add some log :

        if editingStyle == .delete {
            print("Row", indexPath.row)

            deadsticks.remove(at: indexPath.row)
            tableView.deleteRows(at: [indexPath], with: .fade)

           DeadStick.saveDeadSticks(deadsticks)
            print("Row", indexPath.row, Rows in section", tableView.numberOfRows(inSection: indexPath.section))

It doesn't select a row. All of the tableview functions work properly. When I segue back from a modal add the detail view still shows the detail from the last item I selected. That may be a problem but I can live with that. When I delete an item in the tableview I would like it to automatically select the first item in the tableview list. If a row is not selected it will add a duplicate item in the split view in certain cases. I am trying to make sure the user doesn't do this by accident by forcing them to select an item to edit.


The print("Row", indexPath.row) after editing style shows row 3 which is correct.


Your second print statement is giving me an error. I tried print("Row", indexPath.row) after DeadStick.saveDeadSticks(deadsticks) and it also shows row 3.

print("Row", indexPath.row, Rows in section", tableView.numberOfRows(inSection: indexPath.section))


gives what error ? What does it print to log ? What is xxxx

Row 3 Rows in section xxxx

print("Row", indexPath.row, Rows in section", tableView.numberOfRows(inSection: indexPath.section))


You have an extra quote after Rows in section that is causing an error.