Access Container View Controller from Parent iOS
Asked Answered
P

11

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?

Prosecute answered 7/11, 2012 at 22:7 Comment(1)
fully explained here, for modern container views: https://mcmap.net/q/17273/-how-to-add-a-subview-that-has-its-own-uiviewcontroller-in-objective-cPurpure
A
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...
   }
}
Aphonic answered 7/11, 2012 at 22:56 Comment(10)
we aren't segueing? am I missing something here...?Prosecute
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/…Aphonic
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 laterProsecute
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).Aphonic
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.Synopsize
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 iOSNovelette
The cast to AlertViewController is not necessary since the property destinationViewController returns an id.Pyelonephritis
Rather than making arbitrary strings to compare against in prepareForSeque use isKindOfClass instead.Sheets
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 <containingViewControllerClass> { self.containerController = segue.destinationViewControler as <containingViewContollerClass }Codie
the strange terminology "segue" is explained here: https://mcmap.net/q/17273/-how-to-add-a-subview-that-has-its-own-uiviewcontroller-in-objective-cPurpure
A
57

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

Addis answered 8/11, 2012 at 0:11 Comment(4)
@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.Equalitarian
@Fydo, and what is the problem with handling all of the multiple containers on the 'prepare for segue'?Zuzana
What if (horrors!) you decide to switch from storyboard or not use seques, etc. Then you have to dig up code make changes, etc.Canicular
This is my usual approach, but it now crashes for me since I am accessing the childViewControllers "too soon"Margertmargery
J
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
    }
}
Jori answered 29/3, 2015 at 8:2 Comment(1)
What is the use for the question mark after segueName on the if statement? "if segueName?"Nilsson
S
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.

Selfdriven answered 30/4, 2016 at 7:47 Comment(5)
return childViewControllers.filter { $0 is CamperVanViewController }.first in a one linerProsecute
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.Selfdriven
This is a really good solution if you want to access that view controller more than one timeBaxy
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 ... https://mcmap.net/q/17273/-how-to-add-a-subview-that-has-its-own-uiviewcontroller-in-objective-cPurpure
don't filter only to take the first element. just use first(where:). childViewControllers.first(where: { $0 is CamperVanViewController })Footcandle
J
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.

Joejoeann answered 16/10, 2016 at 2:22 Comment(0)
D
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.

Dichasium answered 5/10, 2014 at 9:10 Comment(0)
T
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
  }
}
Trevatrevah answered 30/12, 2016 at 12:16 Comment(0)
A
1

I use Code like:

- (IBAction)showCartItems:(id)sender{ 
  ListOfCartItemsViewController *listOfItemsVC=[self.storyboard instantiateViewControllerWithIdentifier:@"ListOfCartItemsViewController"];
  [self addChildViewController:listOfItemsVC];
 }
Ascot answered 18/5, 2016 at 11:1 Comment(0)
W
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")
}
Welker answered 25/11, 2016 at 1:13 Comment(0)
A
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()!
}
Alcatraz answered 24/4, 2018 at 12:20 Comment(0)
W
0

you can write like this

- (IBAction)showDetail:(UIButton *)sender {  
            DetailViewController *detailVc = [self.childViewControllers firstObject];  
        detailVc.lable.text = sender.titleLabel.text;  
    }  
}
Walke answered 17/8, 2016 at 11:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.