mluton / EmbeddedSwapping

Demonstration of how to make a custom container view controller manage multiple child view controllers using storyboards.
MIT License
208 stars 30 forks source link

`-performSegueWithIdentifier` crashes which I call it in `-viewWillAppear` #11

Closed icodebuster closed 9 years ago

icodebuster commented 9 years ago

Hi, If I call the below code in -viewDidLoad it is working as expected

[self performSegueWithIdentifier:self.currentSegueIdentifier sender:nil];

But when I call it in -viewWillAppear it crashes With the following error

Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'There are unexpected subviews in the container view. Perhaps the embed segue has already fired once or a subview was added programmatically?

My flow is as follows ViewControllerA is having the embedded container, from ViewControllerA I am presenting ViewControllerB When I dismis ViewControllerB, I need to refresh the embeded viewcontroller child so inorder to do that is was callig the -performSegueWithIdentifier in -viewWillAppear but that crashes.

My Query is how do I refresh the childViewControllers or call the -performSegueWithIdentifier in -viewWillAppear.

tikimcfee commented 9 years ago

It's a little tough to say without your code as context, but I experienced something similar a little while ago when I used this styled of embedded view controllers. The first thing to check is to make sure you haven't manually added any subviews to your embedded VC - that doesn't seem to work with container views. Also, calling the method in -viewWillAppear seems like a bit of an odd time to perform a segue. That is usually called after the views hierarchy has been layed out and is ready to show itself. It is probably the case that the segue to another view controller is messing up with the view/controller hierarchy in unexpected ways. Last thing - make sure you're not performing segues manually in an attempt to do a swap. Use the specific methods to perform the swapping. Let me know if any of this advice helped, or if you have more context to give.

icodebuster commented 9 years ago

@tikimcfee

Please take a look at the sample sample code with the same issue. This code is same as the above code just added a new viewcontroller. Please tap the show buton which will take you to the new screen when you tap on close the app crashes.

tikimcfee commented 9 years ago

Ah! Found your problem.

So, if you look carefully at the [self performSegueWithIdentifier:@"embedContainer" sender:nil];, and what it ends up doing, you'll see that it actually manually adds the view controllers and their views to your hierarchy in a pretty clean way - however, notice that this block:

 // If we're going to the first view controller.
    if ([segue.identifier isEqualToString:SegueIdentifierFirst]) {
        // If this is not the first time we're loading this.
        if (self.childViewControllers.count > 0) {
            [self swapFromViewController:[self.childViewControllers objectAtIndex:0] toViewController:self.firstViewController];
        }
        else {
            // If this is the very first time we're loading this we need to do
            // an initial load and not a swap.
            [self addChildViewController:segue.destinationViewController];
            UIView* destView = ((UIViewController *)segue.destinationViewController).view;
            destView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
            destView.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height);
            [self.view addSubview:destView];
            [segue.destinationViewController didMoveToParentViewController:self];
        }
    }
    // By definition the second view controller will always be swapped with the
    // first one.
    else if ([segue.identifier isEqualToString:SegueIdentifierSecond]) {
        [self swapFromViewController:[self.childViewControllers objectAtIndex:0] toViewController:self.secondViewController];
    }

... has no logic to determine what happens if you try to manually perform a segue again; it will end up trying to add views and controllers to the hierarchy where it ought not.

The - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender callback is automatically called for the first time on an embedded container view for you the first time a view controller begins layout of its subviews and it finds a container view that is must present. Calling it again has unintended results here. All you have to do is prevent that from being called again, but only prevent it if the view controller is being reinitialized (i.e., an old VC was deallocated).

To fix your issue, at least in a 'brute force' sort of way, I made three small changes: Add a property: @property (nonatomic, assign) BOOL presenting;

Change viewWillAppear:

- (void)viewWillAppear:(BOOL)animated {
    if(!_presenting)
        [self performSegueWithIdentifier:@"embedContainer" sender:nil];

    _presenting = NO;
}

Change your IBAction:

- (IBAction)showView:(id)sender {
    self.presenting = YES;
    NewViewController *newVC = [[UIStoryboard storyboardWithName:@"MainStoryboard" bundle:nil] instantiateViewControllerWithIdentifier:@"NewViewController"];
    [self presentViewController:newVC animated:YES completion:nil];
}

This will allow you to safely use viewWillAppear to customize your updates on that callback. Let me know if that works for you!

icodebuster commented 9 years ago

@tikimcfee I am extremely sorry, the show button was suppose to be inside the first child view controller. I have updated the code. Your above solution works fine if the show button was in the main view controller. I guess I will have to use delegate callbacks.

Is there a way to reset the embeded container view and again set it since I need to refresh the data in the child view controllers.

yashjain27 commented 4 years ago

You guys are using embedded VC and container vc interchangeably. What's the difference?