Search code examples
iosuiviewuiviewcontrollermkmapviewxib

Programmatically setting an UIViewController's view and life cycle


I have several views in my app targeted for iOS 7+ that show a MKMapView. I'd like to have a single UIViewController to manage those MKMapView views, so I tried to create an UIViewController subclass conforming MKMapViewDelegate protocol:

@interface MapViewController : UIViewController <MKMapViewDelegate>

This MapViewController class has not an associated nib file. Then, in the view controller that manages the view where I want to show a MKMapView:

self.mapController = [[MapViewController alloc] init];
MKMapView *map = [[MKMapView alloc] initWithFrame:CGRectMake(0, 0, 300, 200)];
map.delegate = self.mapController;
[self.mapController setView:map];
[self.view addSubview:self.mapController.view];

This way, I can see that viewWillAppear: method in MapViewController is called, but viewDidLoad: method isn't.

Is this the correct way to do what I want? Why is viewDidLoad: not called?

Thanks in advance


Solution

  • There are two problems here:

    • You shouldn't set the view property of a UIViewController. In fact, you should consider the view hierarchy of a UIViewController as private.
    • you should use Apple's UIViewController containment API to ensure proper invocation of viewWillAppear: etc.

    So here's what you should do:

    The MKMapView must be created in -loadView of MapViewController:

    - (void)loadView
    {
        MKMapView *map = [[MKMapView alloc] initWithFrame:CGRectMake(0, 0, 300, 200)];
        map.delegate = self;
        self.view = map;
    }
    

    When you add the child ViewController to the view hierarchy you should do this:

    self.mapController = [[MapViewController alloc] init];
    [self addChildViewController:self.mapController];
    [self.view addSubview:self.mapController.view];
    [self.mapController didMoveToParentViewController:self];
    

    when you remove the child ViewController from the view hierarchy for whatever reason you should do this:

    [self.mapController willMoveToParentViewController:nil];
    [self.mapController.view removeFromSuperview];
    [self.mapController removeFromParentViewController];
    

    For more details about container view controllers please refer to https://developer.apple.com/library/content/featuredarticles/ViewControllerPGforiPhoneOS/ImplementingaContainerViewController.html

    How does a view controller load its view?

    Apple's view property getter implementation looks a lot like this:

    - (UIView *)view
    {
        [self loadViewIfNeeded];
    
        return _view;
    }
    
    - (void)loadViewIfNeeded
    {
        if (_view == nil) {
            [self loadView];
            [self viewDidLoad];
        }
    }
    

    when you access the view property of a view controller, it checks if view is actually nil. If it is nil, it invokes -loadView and then -viewDidLoad before returning view.

    Therefore when you set the view property before accessing it the first time, -loadView and -viewDidLoad will never be invoked.