210

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?

1

11 Answers 11

367

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...
   }
}
Sign up to request clarification or add additional context in comments.

10 Comments

we aren't segueing? am I missing something here...?
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 developer.apple.com/library/ios/#documentation/uikit/reference/…
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
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).
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.
|
57

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

4 Comments

@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, and what is the problem with handling all of the multiple containers on the 'prepare for segue'?
What if (horrors!) you decide to switch from storyboard or not use seques, etc. Then you have to dig up code make changes, etc.
This is my usual approach, but it now crashes for me since I am accessing the childViewControllers "too soon"
28

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
    }
}

1 Comment

What is the use for the question mark after segueName on the if statement? "if segueName?"
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.

5 Comments

return childViewControllers.filter { $0 is CamperVanViewController }.first in a one liner
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.
This is a really good solution if you want to access that view controller more than one time
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 ... stackoverflow.com/a/23403979/294884
don't filter only to take the first element. just use first(where:). childViewControllers.first(where: { $0 is CamperVanViewController })
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.

Comments

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.

Comments

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
  }
}

Comments

1

I use Code like:

- (IBAction)showCartItems:(id)sender{ 
  ListOfCartItemsViewController *listOfItemsVC=[self.storyboard instantiateViewControllerWithIdentifier:@"ListOfCartItemsViewController"];
  [self addChildViewController:listOfItemsVC];
 }

Comments

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")
}

Comments

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()!
}

Comments

0

you can write like this

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.