207

in iOS6 I noticed the new Container View but am not quite sure how to access it's controller from the containing view.

Scenario:

example

I want to access the labels in Alert view controller from the view controller that houses the container view.

There's a segue between them, can I use that?

Jonas
  • 97,987
  • 90
  • 271
  • 355
Adam Waite
  • 19,748
  • 19
  • 120
  • 146
  • fully explained here, for modern container views: http://stackoverflow.com/a/23403979/294884 – Fattie Dec 12 '16 at 20:50

11 Answers11

366

Yes, you can use the segue to get access the child view controller (and its view and subviews). Give the segue an identifier (such as alertview_embed), using the Attributes inspector in Storyboard. Then have the parent view controller (the one housing the container view) implement a method like this:

- (void) prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
   NSString * segueName = segue.identifier;
   if ([segueName isEqualToString: @"alertview_embed"]) {
       AlertViewController * childViewController = (AlertViewController *) [segue destinationViewController];
       AlertView * alertView = childViewController.view;
       // do something with the AlertView's subviews here...
   }
}
Peter E
  • 4,771
  • 1
  • 17
  • 14
  • 1
    we aren't segueing? am I missing something here...? – Adam Waite Nov 07 '12 at 23:09
  • 25
    yes, there is a embed segue that occurs when the second view controller is made a child of the first view controller. prepareForSegue: is called just before this happens. you can use this opportunity to pass data to the child, or to store a reference to the child for later use. see also http://developer.apple.com/library/ios/#documentation/uikit/reference/UIStoryboardSegue_Class/Reference/Reference.html – Peter E Nov 08 '12 at 01:46
  • 1
    Ah right, does 'second view controller is made a child of the first view controller' when the view loads? This is making more sense now, thanks. I'm not with my project now but will test later – Adam Waite Nov 08 '12 at 11:23
  • 1
    exactly, it is called before viewDidLoad. By the time viewDidLoad is reached, the parent and child have been connected and [self childViewControllers] in the parent will return an array of all the child controllers (see rdelmar's answer below). – Peter E Nov 08 '12 at 13:14
  • 2
    I would add one caveat to the proposed solution: be very careful when accessing the (child) destination view controller's view property: in some circumstances this will cause its viewDidLoad to be called there and then.I would recommend setting up any needed segue data beforehand so that the viewDidLoad can fire safely. – AlwaysLearning Nov 12 '12 at 00:44
  • can you please explain this line.. AlertView * alertView = childViewController.view; What is "AlertView" referring to? is this the Parent view Controller?Sorry, im new to iOS – Sarim Sidd Apr 26 '13 at 07:07
  • The cast to AlertViewController is not necessary since the property destinationViewController returns an id. – Hamal000 May 28 '13 at 17:40
  • Rather than making arbitrary strings to compare against in prepareForSeque use `isKindOfClass` instead. – memmons Sep 23 '14 at 14:11
  • But @MichaelG.Emmons - naming segues in storyboard is S.O.P. What if prepareForSegue() had other types of segues where the destination controller type was the same, but perhaps different instances? In that case you'd want to be able to know which was which and the segue.id would be the way to do that. Maybe it doesn't happen ever in real life, but still.. Anyway, the type check in Swift: if segue.destinationViewController is { self.containerController = segue.destinationViewControler as – clearlight Feb 02 '15 at 14:52
  • the strange terminology **"segue"** is explained here: http://stackoverflow.com/a/23403979/294884 – Fattie May 25 '17 at 12:22
57

You can do that simply with self.childViewControllers.lastObject (assuming you only have one child, otherwise use objectAtIndex:).

elp
  • 7,607
  • 7
  • 58
  • 113
rdelmar
  • 102,832
  • 11
  • 203
  • 218
  • 1
    @RaphaelOliveira, not necessarily. If you have multiple childControllers in a single view, THIS would be the preferred approach. It lets you co-ordinate multiple containers at once. prepareForSegue only has reference to the single child controller instance it's acting on. – Fydo Jun 04 '14 at 12:05
  • 2
    @Fydo, and what is the problem with handling all of the multiple containers on the 'prepare for segue'? – Lay González Jun 20 '14 at 17:54
  • 1
    What if (horrors!) you decide to switch from storyboard or not use seques, etc. Then you have to dig up code make changes, etc. – Tom Andersen Apr 15 '15 at 20:23
  • 2
    This is my usual approach, but it now crashes for me since I am accessing the `childViewControllers` "too soon" – Mazyod May 04 '16 at 15:02
27

for Swift Programming

you can write like this

var containerViewController: ExampleViewController?
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    // you can set this name in 'segue.embed' in storyboard
    if segue.identifier == "checkinPopupIdentifierInStoryBoard" {
        let connectContainerViewController = segue.destinationViewController as ExampleViewController
        containerViewController = connectContainerViewController
    }
}
Sruit A.Suk
  • 6,247
  • 7
  • 55
  • 66
19

The prepareForSegue approach works, but it relies on the segue identifier magic string. Maybe there's a better way.

If you know the class of the VC you're after, you can do this very neatly with a computed property:

var camperVan: CamperVanViewController? {
  return childViewControllers.flatMap({ $0 as? CamperVanViewController }).first
  // This works because `flatMap` removes nils
}

This relies on childViewControllers. While I agree it could be fragile to rely on the first one, naming the class you seek makes this seem quite solid.

SimplGy
  • 18,875
  • 14
  • 95
  • 138
  • 3
    `return childViewControllers.filter { $0 is CamperVanViewController }.first` in a one liner – Adam Waite May 04 '16 at 10:46
  • 1
    I've since done `childViewControllers.flatMap({ $0 as? CamperVanViewController }).first` which I think is a little better, since it casts and gets rid of any nils. – SimplGy May 04 '16 at 17:14
  • This is a really good solution if you want to access that view controller more than one time – Gabriel Goncalves Jul 11 '16 at 04:39
  • this is hopeless - there's no particular reason you may have only one of that particular class. that's exactly why identifiers exist. just follow the standard formula ... http://stackoverflow.com/a/23403979/294884 – Fattie May 25 '17 at 12:23
  • don't filter only to take the first element. just use `first(where:)`. `childViewControllers.first(where: { $0 is CamperVanViewController })` – Alexander Sep 06 '17 at 20:11
9

An updated answer for Swift 3, using a computed property:

var jobSummaryViewController: JobSummaryViewController {
    get {
        let ctrl = childViewControllers.first(where: { $0 is JobSummaryViewController })
        return ctrl as! JobSummaryViewController
    }
}

This only iterates the list of children until it reaches the first match.

Robin Daugherty
  • 5,698
  • 1
  • 40
  • 50
8

self.childViewControllers is more relevant when you need control from the parent. For instance, if the child controller is a table view and you want to reload it forcefully or change a property via a button tap or any other event on Parent View Controller, you can do it by accessing ChildViewController's instance and not via prepareForSegue. Both have their applications in different ways.

Gautam Jain
  • 2,883
  • 28
  • 25
2

There is another way using Swift's switch statement on the type of the view controller :

override func prepare(for segue: UIStoryboardSegue, sender: Any?)
{
  switch segue.destination
  {
    case let aViewController as AViewController:
      self.aViewController = aViewController
    case let bViewController as BViewController:
      self.bViewController = bViewController
    default:
      return
  }
}
Joanna Carter
  • 163
  • 2
  • 7
1

I use Code like:

- (IBAction)showCartItems:(id)sender{ 
  ListOfCartItemsViewController *listOfItemsVC=[self.storyboard instantiateViewControllerWithIdentifier:@"ListOfCartItemsViewController"];
  [self addChildViewController:listOfItemsVC];
 }
Mannam Brahmam
  • 2,059
  • 2
  • 20
  • 35
1

In case someone is looking for Swift 3.0,

viewController1, viewController2 and so on will then be accessible.

let viewController1 : OneViewController!
let viewController2 : TwoViewController!

// Safety handling of optional String
if let identifier: String = segue.identifier {

    switch identifier {

    case "segueName1":
        viewController1 = segue.destination as! OneViewController
        break

    case "segueName2":
        viewController2 = segue.destination as! TwoViewController
        break

    // ... More cases can be inserted here ...

    default:
        // A new segue is added in the storyboard but not yet including in this switch
        print("A case missing for segue identifier: \(identifier)")
        break
    }

} else {
    // Either the segue or the identifier is inaccessible 
    print("WARNING: identifier in segue is not accessible")
}
Marco Leong
  • 513
  • 4
  • 10
1

With generic you can do some sweet things. Here is an extension to Array:

extension Array {
    func firstMatchingType<Type>() -> Type? {
        return first(where: { $0 is Type }) as? Type
    }
}

You can then do this in your viewController:

var viewControllerInContainer: YourViewControllerClass? {
    return childViewControllers.firstMatchingType()!
}
Sunkas
  • 8,816
  • 5
  • 55
  • 94
0

you can write like this

- (IBAction)showDetail:(UIButton *)sender {  
            DetailViewController *detailVc = [self.childViewControllers firstObject];  
        detailVc.lable.text = sender.titleLabel.text;  
    }  
}
Khurshid
  • 101
  • 7