Agregar un controlador de vista como una subvista en otro controlador de vista

81

He encontrado algunas publicaciones para este problema, pero ninguna resolvió mi problema.

Di como si ...

  1. ViewControllerA
  2. ViewControllerB

Intenté agregar ViewControllerB como una subvista en ViewControllerA pero arroja un error como " fatal error: unexpectedly found nil while unwrapping an Optional value".

A continuación se muestra el código ...

ViewControllerA

var testVC: ViewControllerB = ViewControllerB();

override func viewDidLoad()
{
    super.viewDidLoad()
    self.testVC.view.frame = CGRectMake(0, 0, 350, 450);
    self.view.addSubview(testVC.view);
    // Do any additional setup after loading the view.
}

ViewControllerB es solo una pantalla simple con una etiqueta en ella.

ViewControllerB

 @IBOutlet weak var test: UILabel!

override func viewDidLoad() {
    super.viewDidLoad()
    test.text = "Success" // Throws ERROR here "fatal error: unexpectedly found nil while unwrapping an Optional value"
}

EDITAR

Con la solución sugerida de las respuestas del usuario, ViewControllerB en ViewControllerA está saliendo de la pantalla. El borde gris es el marco que he creado para la subvista. ingrese la descripción de la imagen aquí

Srujan Simha
fuente

Respuestas:

176

Un par de observaciones:

  1. Cuando crea una instancia del segundo controlador de vista, está llamando ViewControllerB(). Si ese controlador de vista crea su vista mediante programación (lo cual es inusual) estaría bien. Pero la presencia de IBOutletsugiere que la escena de este segundo controlador de vista se definió en Interface Builder, pero al llamar ViewControllerB(), no le está dando al guión gráfico la oportunidad de instanciar esa escena y conectar todos los puntos de venta. Por lo tanto, lo implícitamente desenvuelto UILabeles nil, lo que resulta en su mensaje de error.

    En su lugar, desea darle a su controlador de vista de destino un "ID de guión gráfico" en Interface Builder y luego puede usarlo instantiateViewController(withIdentifier:)para crear una instancia (y conectar todos los puntos de venta de IB). En Swift 3:

    let controller = storyboard!.instantiateViewController(withIdentifier: "scene storyboard id")
    

    Ahora puede acceder a esta controller's view.

  2. Pero si realmente quieres hacerlo addSubview(es decir, no estás pasando a la siguiente escena), entonces estás participando en una práctica llamada "contención del controlador de vista". No solo quieres hacerlo simplemente addSubview. Desea hacer algunas llamadas adicionales al controlador de vista de contenedor, por ejemplo:

    let controller = storyboard!.instantiateViewController(withIdentifier: "scene storyboard id")
    addChild(controller)
    controller.view.frame = ...  // or, better, turn off `translatesAutoresizingMaskIntoConstraints` and then define constraints for this subview
    view.addSubview(controller.view)
    controller.didMove(toParent: self)
    

    Para obtener más información sobre por qué esto addChild(anteriormente llamado addChildViewController) y didMove(toParent:)(anteriormente llamado didMove(toParentViewController:)) son necesarios, consulte el video de WWDC 2011 # 102: Implementación de la contención de UIViewController . En resumen, debe asegurarse de que la jerarquía de su controlador de vista se mantenga sincronizada con su jerarquía de vista, y estas llamadas addChildy didMove(toParent:)garantizar que este sea el caso.

    Consulte también Creación de controladores de vista de contenedor personalizados en la Guía de programación de controladores de vista.


Por cierto, lo anterior ilustra cómo hacer esto mediante programación. En realidad, es mucho más fácil si usa la "vista de contenedor" en Interface Builder.

ingrese la descripción de la imagen aquí

Entonces no tiene que preocuparse por ninguna de estas llamadas relacionadas con la contención, y Interface Builder se encargará de ello por usted.

Para la implementación de Swift 2, consulte la revisión anterior de esta respuesta .

Robar
fuente
1
Gracias por la explicación detallada. Cuando intenté agregar ViewControllerBa ViewControllerA, ViewControllerBse apaga la pantalla. He editado mi publicación con la captura de pantalla del simulador.
Srujan Simha
Eso es posible. Por eso, en mi ejemplo, configuré framemanualmente. O si lo apaga translatesFrameIntoConstraints(o como se llame), y probablemente también pueda agregar restricciones mediante programación. Pero si está agregando la subvista, es responsable de configurar su marco, de una forma u otra, al igual que para todas las subvistas agregadas mediante programación.
Rob
1
Así es como se pueden agregar límitescontroller.view.frame = UIScreen.mainScreen().bounds
Codetard
3
Al realizar la contención del controlador de vista, realmente debe hacer referencia a la supervista, no a la pantalla. Francamente, ahora que tenemos la multitarea de pantalla dividida, no es aconsejable hacer nada que haga referencia a la pantalla.
Rob
1
@Honey - No estoy seguro de lo que quiere decir con "asumir que toda la vista de viewController es solo una subvista del parentViewController". Por definición, cuando lo hace addSubview, la vista raíz del controlador secundario es una subvista de la vista a la que lo agregó. Todo lo que debe hacer es agregar restricciones entre la vista raíz del controlador secundario y la vista a la que acaba de agregarlo como una subvista.
Rob
48

Gracias a Rob. Añadiendo sintaxis detallada para su segunda observación:

let controller:MyView = self.storyboard!.instantiateViewControllerWithIdentifier("MyView") as! MyView
controller.ANYPROPERTY=THEVALUE // If you want to pass value
controller.view.frame = self.view.bounds
self.view.addSubview(controller.view)
self.addChildViewController(controller)
controller.didMoveToParentViewController(self)

Y para eliminar el viewcontroller:

self.willMoveToParentViewController(nil)
self.view.removeFromSuperview()
self.removeFromParentViewController() 
SML
fuente
6
Por cierto, cuando llame addChildViewController, no llame willMoveToParentViewController. Llamar addChildViewControllerlo llamará por ti. Consulte Agregar y eliminar un niño en la Guía de programación de View Controller para iOS. Por esta razón, personalmente siempre llamaría addChildViewControllerinmediatamente después de crear una instancia, pero antes de configurarlo.
Rob
1
Cuando haces controller.ANYPROPERTY = THEVALUE..Supongo que AnyProperty está definido en childViewController. Lo probé y me estaba dando un error. Alguna idea de cómo rectificar eso.
Anuj Arora
@Anuj Arora, tu conjetura es correcta. ANYPROPERTY se define en el controlador de vista secundario. Puede marcar CUALQUIER PROPIEDAD en el controlador de vista secundario, pero en ViewDidAppear no en ViewDidLoad.
Sunita
7
This code will work for Swift 4.2.

let controller:SecondViewController = 
self.storyboard!.instantiateViewController(withIdentifier: "secondViewController") as! 
SecondViewController
controller.view.frame = self.view.bounds;
self.view.addSubview(controller.view)
self.addChild(controller)
controller.didMove(toParent: self)
Nirbhay Singh
fuente
No , no llamar willMove. addChildhace eso por ti. Consulte la willMove documentación . Entonces la secuencia es (1) addChild; (2) configure la vista del vc secundario y agréguela a la jerarquía de vista; y (3) llamardidMove(toParent:)
Rob
4

Para agregar y quitar ViewController

 var secondViewController :SecondViewController?

  // Adding 
 func add_ViewController() {
    let controller  = self.storyboard?.instantiateViewController(withIdentifier: "secondViewController")as! SecondViewController
    controller.view.frame = self.view.bounds
    self.view.addSubview(controller.view)
    self.addChild(controller)
    controller.didMove(toParent: self)
    self.secondViewController = controller
}

// Removing
func remove_ViewController(secondViewController:SecondViewController?) {
    if secondViewController != nil {
        if self.view.subviews.contains(secondViewController!.view) {
             secondViewController!.view.removeFromSuperview()
        }
        
    }
}
krishnan
fuente
No llames willMove. Como dice la willMove documentación , lo addChildhace por usted.
Rob
3

func callForMenuView () {

    if(!isOpen)

    {
        isOpen = true

        let menuVC : MenuViewController = self.storyboard!.instantiateViewController(withIdentifier: "menu") as! MenuViewController
        self.view.addSubview(menuVC.view)
        self.addChildViewController(menuVC)
        menuVC.view.layoutIfNeeded()

        menuVC.view.frame=CGRect(x: 0 - UIScreen.main.bounds.size.width, y: 0, width: UIScreen.main.bounds.size.width-90, height: UIScreen.main.bounds.size.height);

        UIView.animate(withDuration: 0.3, animations: { () -> Void in
            menuVC.view.frame=CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width-90, height: UIScreen.main.bounds.size.height);
    }, completion:nil)

    }else if(isOpen)
    {
        isOpen = false
      let viewMenuBack : UIView = view.subviews.last!

        UIView.animate(withDuration: 0.3, animations: { () -> Void in
            var frameMenu : CGRect = viewMenuBack.frame
            frameMenu.origin.x = -1 * UIScreen.main.bounds.size.width
            viewMenuBack.frame = frameMenu
            viewMenuBack.layoutIfNeeded()
            viewMenuBack.backgroundColor = UIColor.clear
        }, completion: { (finished) -> Void in
            viewMenuBack.removeFromSuperview()

        })
    }
jaya
fuente
1

Gracias a Rob, se actualizó la sintaxis de Swift 4.2

let controller:WalletView = self.storyboard!.instantiateViewController(withIdentifier: "MyView") as! WalletView
controller.view.frame = self.view.bounds
self.view.addSubview(controller.view)
self.addChild(controller)
controller.didMove(toParent: self)
Emre Gürses
fuente
usar "controller.view.frame = self.view.bounds" en lugar de "controller.view.frame = self.view.frame" ¡funciona para mí!
Mehdico
No llames willMove. Como dice la willMove documentación , lo addChildhace por usted. No se obtiene nada bueno de llamarlo dos veces.
Rob
0

Consulte también la documentación oficial sobre la implementación de un controlador de vista de contenedor personalizado:

https://developer.apple.com/library/content/featuredarticles/ViewControllerPGforiPhoneOS/ImplementingaContainerViewController.html#//apple_ref/doc/uid/TP40007457-CH11-SW1

Esta documentación tiene información mucho más detallada para cada instrucción y también describe cómo agregar transiciones.

Traducido a Swift 3:

func cycleFromViewController(oldVC: UIViewController,
               newVC: UIViewController) {
   // Prepare the two view controllers for the change.
   oldVC.willMove(toParentViewController: nil)
   addChildViewController(newVC)

   // Get the start frame of the new view controller and the end frame
   // for the old view controller. Both rectangles are offscreen.r
   newVC.view.frame = view.frame.offsetBy(dx: view.frame.width, dy: 0)
   let endFrame = view.frame.offsetBy(dx: -view.frame.width, dy: 0)

   // Queue up the transition animation.
   self.transition(from: oldVC, to: newVC, duration: 0.25, animations: { 
        newVC.view.frame = oldVC.view.frame
        oldVC.view.frame = endFrame
    }) { (_: Bool) in
        oldVC.removeFromParentViewController()
        newVC.didMove(toParentViewController: self)
    }
}
Simon Backx
fuente