13

I have a mainViewController. I call [self pushModalViewController:someViewController] which makes someViewController the active view.

Now I want to call a function in mainViewController as someViewController disappears with [self dismissModalViewController].

viewDidAppear does not get called probably because the view was already there, just beneath the modal view. How does one go about calling a function in the mainViewController once the modalView dismisses itself?

Thanks a lot!

Bryan
  • 17,201
  • 24
  • 97
  • 123

3 Answers3

32

This answer was rewritten/expanded to explain the 3 most important approaches (@galambalazs)

1. Blocks

The simplest approach is using a callback block. This is good if you only have one listener (the parent view controller) interested in the dismissal. You may even pass some data with the event.

In MainViewController.m

SecondViewController* svc = [[SecondViewController alloc] init];
svc.didDismiss = ^(NSString *data) {
    // this method gets called in MainVC when your SecondVC is dismissed
    NSLog(@"Dismissed SecondViewController");
};
[self presentViewController:svc animated:YES completion:nil];

In SecondViewController.h

@interface MainViewController : UIViewController
    @property (nonatomic, copy) void (^didDismiss)(NSString *data);
    // ... other properties
@end

In SecondViewController.m

- (IBAction)close:(id)sender 
{
    [self dismissViewControllerAnimated:YES completion:nil];

    if (self.didDismiss) 
        self.didDismiss(@"some extra data");
}

2. Delegation

Delegation is the recommended pattern by Apple:

Dismissing a Presented View Controller

If the presented view controller must return data to the presenting view controller, use the delegation design pattern to facilitate the transfer. Delegation makes it easier to reuse view controllers in different parts of your app. With delegation, the presented view controller stores a reference to a delegate object that implements methods from a formal protocol. As it gathers results, the presented view controller calls those methods on its delegate. In a typical implementation, the presenting view controller makes itself the delegate of its presented view controller.

MainViewController

In MainViewController.h

@interface MainViewController : UIViewController <SecondViewControllerDelegate>
    - (void)didDismissViewController:(UIViewController*)vc;
    // ... properties
@end

Somewhere in MainViewController.m (presenting)

SecondViewController* svc = [[SecondViewController alloc] init];
svc.delegate = self;
[self presentViewController:svc animated:YES completion:nil];

Somewhere else in MainViewController.m (being told about the dismissal)

- (void)didDismissViewController:(UIViewController*)vc
{
    // this method gets called in MainVC when your SecondVC is dismissed
    NSLog(@"Dismissed SecondViewController");
}

SecondViewController

In SecondViewController.h

@protocol SecondViewControllerDelegate <NSObject>
- (void)didDismissViewController:(UIViewController*)vc;
@end

@interface SecondViewController : UIViewController
@property (nonatomic, weak) id<SecondViewControllerDelegate> delegate;
// ... other properties
@end

Somewhere in SecondViewController.m

[self.delegate myActionFromViewController:self];
[self dismissViewControllerAnimated:YES completion:nil];

(note: the protocol with didDismissViewController: method could be reused throughout your app)


3. Notifications

Another solution is sending an NSNotification. This is a valid approach as well, it might be easier than delegation in case you only want to notify about the dismissal without passing much data. But it's main use case is when you want multiple listeners for the dismissal event (other than just the parent view controller).

But make sure to always remove yourself from NSNotificationCentre after you are done! Otherwise you risk of crashing by being called for notifications even after you are deallocated. [editor's note]

In MainViewController.m

- (IBAction)showSecondViewController:(id)sender 
{
    SecondViewController *secondVC = [[SecondViewController alloc] init];
    [self presentViewController:secondVC animated:YES completion:nil];

    // Set self to listen for the message "SecondViewControllerDismissed"
    // and run a method when this message is detected
    [[NSNotificationCenter defaultCenter] 
     addObserver:self
     selector:@selector(didDismissSecondViewController)
     name:@"SecondViewControllerDismissed"
     object:nil];
}

- (void)dealloc
{
    // simply unsubscribe from *all* notifications upon being deallocated
    [[NSNotificationCenter defaultCenter] removeObserver:self];
} 

- (void)didDismissSecondViewController 
{
    // this method gets called in MainVC when your SecondVC is dismissed
    NSLog(@"Dismissed SecondViewController");
}

In SecondViewController.m

- (IBAction)close:(id)sender 
{
    [self dismissViewControllerAnimated:YES completion:nil];

    // This sends a message through the NSNotificationCenter 
    // to any listeners for "SecondViewControllerDismissed"
    [[NSNotificationCenter defaultCenter] 
     postNotificationName:@"SecondViewControllerDismissed" 
     object:nil userInfo:nil];
}

Hope this helps!

Community
  • 1
  • 1
Grimless
  • 1,268
  • 1
  • 15
  • 30
  • When I change everything to use main delegate: [someViewController setMainDelegate:self]; [self pushModalViewController:someViewController]; [mainDelegate someActionFromViewController:self]; [self dismissModalViewController]; ...... it will build but crash Are you sure this syntax works? Do I need to declare the delegate? – Bryan Nov 11 '10 at 02:07
  • Sorry, yes you will have to declare the delegate in your @interface section. I also suggest creating a protocol to make sure you calling an object that can handle the message you are sending. So id delegate; – Grimless Nov 11 '10 at 02:40
  • I used this link to create the protocol and declaration. http://stackoverflow.com/questions/645449/how-to-use-custom-delegates-in-objective-c – Bryan Nov 11 '10 at 03:27
  • @protocol PropertiesControllerDelegate -- id delegate; -- But I am still getting a crash at the setDelegate method.....-[PropertiesController setDelegate:]: unrecognized selector sent to instance 0x177840 2010-11-10 19:27:29.463 iScoutShoes[4142:307] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[PropertiesController setDelegate:]: unrecognized selector sent to instance 0x177840' – Bryan Nov 11 '10 at 03:29
  • Do I need to add the protocol in the mainViewController class too? I can't seem to figure this out. Thanks so much for all of your help already. – Bryan Nov 11 '10 at 03:55
  • So, the link you provided looked pretty good. Notice that they have an `@property` declaration and presumably a `@synthesize` as well. You will have to do that in your PropertiesController class. The @property/@synthesize pair will make a getter and setter for your `id delegate;` field inside of your PropertiesController. Hope that helps! – Grimless Nov 11 '10 at 04:00
  • Edited answer to hopefully illustrate better. Hope that's a little better. – Grimless Nov 11 '10 at 04:09
7

Using exit (unwind) segue

When you're using storyboards and segues you can use a very handy approach with minimal code to dismiss a modal view controller and inform the underlying view controller that the modal view controller has been closed.

Using exit (unwind) segues you gain 3 advantages:

  1. you don't need to write any code to dismiss the modal view controller and
  2. you can have iOS call a callback method inside the underlying view controller that has presented the model view controller.
  3. you use the very same semantics that you already know from implementing prepareForSegue:

Implement it with only 2 steps

  1. Create an action method in the Parent view controller that presents another (modal) view controller:

Swift

@IBAction func unwindFromSegue(segue: UIStoryboardSegue) {
    print("Unwind from segue", segue.identifier)
}

Objective-C

- (IBAction)unwindFromSegue:(UIStoryboardSegue *)segue {
    NSLog(@"Unwind from segue %s", segue.identifier);
}
  1. In the storyboard, On child view controller right click the exit segue (aka unwind segue, it's the last of the icons at the top of your view controller), drag & drop unwindFromSegue: to your button and select action.

enter image description here

You're done! Now the modal view controller closes when you click the dismiss button and unwindFromSegue: informs your underlying view controller(Parent) that the modal view controller(Child) has closed.

Mamad Farrahi
  • 394
  • 1
  • 5
  • 20
Lars Blumberg
  • 19,326
  • 11
  • 90
  • 127
1

Here's a callback solution which takes less modifications to your modal and parent: In the Model's .h add:

@property (nonatomic, copy) void (^dismissed)();

In the Model's .m put this in the completion when you dismiss the modal:

 [self dismissViewControllerAnimated:YES completion:^{
    if(self.dismissed)
        self.dismissed();
}];

In the parent view controller when you instantiate your modal set the dismissed callback:

Modal = //Init your modal
[Modal setDismissed:^{
   //do stuff you wanted when it's dimissed
}];
 [self presentViewController:Modal animated:YES completion:nil];
Entrabiter
  • 752
  • 7
  • 13