-1

High level summary of my issue:

I have a UITableViewController (TVC1) that does a performSegue to invoke a static UITableViewController (TVC2) so the user can enter details. After pressing a button called "Add another" on TVC2, I want TVC2 to save its data and to be invoked again to add another record.

Details:

I have a tableViewController (TVC1) which implements this data model:

- Category <=> maps to tableview sections
  - Group <=> maps to tableview rows
    - Detail <=> maps to tableview rows

A display could look like this:

- Section 0: Category 1
  - Row 0: Group A
  - Row 1: Detail x1
  - Row 2: Detail y2
  - Row 3: Detail z3
  - Row 4: Group B
  - Row 5: Detail x2
  - Row 6: Detail y2
  - Row 7: Detail y3

When I want to edit a detail by clicking on detail row 5 for example, I do a performSegue in the didSelectRowAt.

In the TVC1.prepareForSegue I provide TVC1 as the delegate for TVC2 where that detail can be added/edited. I also provide the data of the detail to be edited to properties of TVC2.

On TVC2 - after providing the detail - the user can click a save button in the toolbar, which will save the data and take him back to TVC1. Alternatively, at the end of the form where the details can be entered, I have a button:

  • Add another detail

The idea is that the user is directly taken to a new screen where they can add another detail for the same group. So basically after TVC2 disappears, it should appear again to add new details.

In TVC2, I linked this button to the unwindFrom method on TVC1. Then in the prepareForSegue of TVC2, I call a protocol method on the delegate (TVC1) to provide back the edited details and provide a structure called NextAction to inform the root TVC1 what its next action should be. In this case it should add another detail in the same group.

Then in the unwindFrom method of TVC1 I look at the nextAction and initiate the next performSegue.

I don't know of another place to do this. For me that is the last hook where I can present the next action, which is TVC2 again.

This results in weird behaviour. It appears to work once, but not a second time.

Debugging shows (=====> lines are my own print debug lines):

========> TVC2 prepare start (means the add more details button was pressed)
========> TVC1 newDetailValues begin (TVC2 calling the delegate protocol method to provide back edited data - including NextAction)
========> TVC1 newDetailValues end
========> TVC2 prepare end (I am not sure if TVC2 is now closed/inaccessible)
========> TVC1 unwindFromAddEdit begin (here nextAction is evaluated and a performSegue done)
========> TVC1 prepare begin (initiated by the performSegue)
========> TVC1 prepare end
'Presenting view controllers on detached view controllers is discouraged <Project.TVC1: 0x.........>.'
========> TVC1 unwindFromAddEdit end

Note the error/warning:

'Presenting view controllers on detached view controllers is discouraged <Project.TVC1: 0x.........>.'

But I do see that detail TVC2. But when I click on the button again, it displays the root TVC1 and not TVC2. xcode now says:

'Warning: Attempt to present <UINavigationController: 0x.........> on <Project.TVC1: 0x.........> whose view is not in the window hierarchy!'

I got the feeling this has to to with threading - possibly where the TVC1.unwindFrom is running in the scope of TVC2.

When I edit TVC1.unwindFrom and replace:

performSegue(withIdentifier: "AddDetails", sender: self)

... with:

DispatchQueue.main.async {
   performSegue(withIdentifier: "AddDetails", sender: self)
}

Everything works just fine.

But:

  1. I don't understand what is happening.
  2. I can't assess if what I'm doing is thread safe. For example: Am I introducing a race condition between TVC2 closing and TVC1 calling TVC2 at the same time?

Who can shine some light into what is happening?

  • I wouldn’t unwind in that case. I would push another instance of TVC2 to add the new item and then when you hit save in that instance unwind back to TVC1 – Paulw11 Nov 02 '17 at 12:51
  • It is not a problem with threading. You are presenting view controller (NavigationController) on view controller (Project.TVC1) whose view is not in window (view hierarchy). It means that TVC1 is about to removed or even removed. – Damian Rzeszot Nov 02 '17 at 16:02
  • @Paulw11: Do you mean: After TVC1 invoked TVC2 through performSegue and the user clicked the "save and add another" button on TVC2, I execute a segue from that button on TVC2 to its own controller TVC2? Would this keep the previous TVC2 object alive while the user works in the 2nd TVC2 instance? Wouldn't I build up a stack of TVC2's in that case? – Arjen Hiemstra Nov 02 '17 at 20:41
  • @DamianRzeszot: For the edit detail segue on TVC1 I do a Show Segue of TVC2 directly. For the add detail I do a Present Modally Segue for the Navigation Controller of TVC2. So the order of events is: TVC1 => Show Segue of TVC2 => unwind to TVC1 => in unwind: Present Modally Segue NavContr of TVC2 => unwind to TVC1 => in unwind: Present Modally Segue NavContr TVC2 etc. – Arjen Hiemstra Nov 02 '17 at 20:56
  • Yes, you would have a stack of TVC2 but then when you unwind back to TVC1 they would be released. That is the beauty of an unwind segue; it can easily unwind any number of view controllers to get back to where you want to be. – Paulw11 Nov 02 '17 at 21:08
  • The other option if you want to have a "save and add another" function is just to reset the state of the current view controller; no need to dismiss or present anything. – Paulw11 Nov 02 '17 at 21:10
  • I was expecting that when the unwind executes TVC2 has ended completely and the 2nd and subsequent Present Modally Segue executed from the unwind would execute just like the first one that was executed from the DidSelectRowAt. The question is: What is the status of the navigation controllers and table view controllers when unwind is being executed. I get the feeling the unwind is executed like a delegate from TVC2. – Arjen Hiemstra Nov 02 '17 at 21:29
  • @Paulw11: Where would you push another instance of TVC2? When I would save that 2nd instance of TVC2 normally (without add another) and I would unwind, would that unwind unwind the complete stack? So I could basically have a normal action for the "add another" button where I present the VC again? – Arjen Hiemstra Nov 02 '17 at 21:34
  • I would push the new instance of TVC2 in the "Add another detail" button handler (or perhaps just reset the state of the current TVC2 and use that). Read Apple's [technote](https://developer.apple.com/library/content/technotes/tn2298/_index.html) to understand how Unwind segue's work; You target the unwind method by name and the unwind segue will dismiss any view controllers it needs to to get there. – Paulw11 Nov 02 '17 at 21:36

1 Answers1

0

Answer was provided by Paulw11 in the comments:

Best way is not to unwind after each invocation of TVC2 when the "add another" button is pressed.

Instead, the "add another" button should be control-dragged to the same navigation controller and do a present modally.

In the prepareForSegue the details that TVC2 needs can be copied from the current ViewController to the new instance of itself.

At some point the user uses either the CANCEL or SAVE button in the toolbar, which unwinds everything to TVC1.

Doing it this way also makes the UI look cleaner, as the unwind to TVC1 is not visible.