Creando una UIView reutilizable con xib (y cargando desde el guión gráfico)

81

De acuerdo, hay docenas de publicaciones en StackOverflow sobre esto, pero ninguna es particularmente clara sobre la solución. Me gustaría crear un UIViewarchivo personalizado con un archivo xib adjunto. Los requisitos son:

  • No por separado UIViewController: una clase completamente autónoma
  • Puntos de venta en la clase para permitirme establecer / obtener propiedades de la vista

Mi enfoque actual para hacer esto es:

  1. Anular -(id)initWithFrame:

    -(id)initWithFrame:(CGRect)frame {
        self = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                              owner:self
                                            options:nil] objectAtIndex:0];
        self.frame = frame;
        return self;
    }
    
  2. Crear una instancia mediante programación usando -(id)initWithFrame:en mi controlador de vista

    MyCustomView *myCustomView = [[MyCustomView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, self.view.bounds.size.height)];
    [self.view insertSubview:myCustomView atIndex:0];
    

Esto funciona bien (aunque nunca llamar [super init]y simplemente configurar el objeto usando el contenido de la punta cargada parece un poco sospechoso; aquí hay un consejo para agregar una subvista en este caso que también funciona bien). Sin embargo, también me gustaría poder crear una instancia de la vista desde el guión gráfico. Así que puedo:

  1. Coloque una UIViewvista principal en el guión gráfico
  2. Establezca su clase personalizada en MyCustomView
  3. Anular -(id)initWithCoder:: el código que he visto con mayor frecuencia se ajusta a un patrón como el siguiente:

    -(id)initWithCoder:(NSCoder *)aDecoder {
        self = [super initWithCoder:aDecoder];
        if (self) {
            [self initializeSubviews];
        }
        return self;
    }
    
    -(id)initWithFrame:(CGRect)frame {
        self = [super initWithFrame:frame];
        if (self) {
            [self initializeSubviews];
        }
        return self;
    }
    
    -(void)initializeSubviews {
        typeof(view) view = [[[NSBundle mainBundle]
                             loadNibNamed:NSStringFromClass([self class])
                                    owner:self
                                  options:nil] objectAtIndex:0];
        [self addSubview:view];
    }
    

Por supuesto, esto no funciona, ya que si uso el enfoque anterior o si hago una instancia programática, ambos terminan llamando de forma recursiva -(id)initWithCoder:al ingresar -(void)initializeSubviewsy cargar la punta del archivo.

Varias otras preguntas de SO tratan con esto, como aquí , aquí , aquí y aquí . Sin embargo, ninguna de las respuestas dadas soluciona satisfactoriamente el problema:

  • Una sugerencia común parece ser incrustar toda la clase en un UIViewController y hacer la carga de la punta allí, pero esto me parece subóptimo, ya que requiere agregar otro archivo solo como un contenedor

¿Alguien podría dar un consejo sobre cómo resolver este problema y hacer que los tomacorrientes funcionen de manera personalizada UIViewcon un mínimo de problemas / sin una envoltura de controlador delgada? ¿O hay una forma alternativa y más limpia de hacer las cosas con un código estándar mínimo?

Ken Chatfield
fuente
1
¿Alguna vez obtuvo una respuesta satisfactoria para esto? Estoy luchando por esto en este momento. Todas las demás respuestas no parecen lo suficientemente buenas, como mencionas. Siempre puede responder la pregunta usted mismo si ha descubierto algo en los últimos meses.
Mike Meyers
13
¿Por qué es tan difícil crear vistas reutilizables en iOS?
relojero
1
De hecho, la respuesta a la que se vincula utiliza exactamente el mismo enfoque (aunque su respuesta no incluye una función init de rect, lo que significa que solo se puede inicializar desde el guión gráfico y no programáticamente)
Ken Chatfield
1
con respecto a este antiguo control de calidad, Apple finalmente introdujo REFERENCIAS DE STORYBOARD ... developer.apple.com/library/ios/recipes/… ... así que eso es todo, ¡uf!
Fattie

Respuestas:

13

Tu problema es llamar loadNibNamed:de (un descendiente de) initWithCoder:. loadNibNamed:llamadas internas initWithCoder:. Si desea anular el codificador del guión gráfico y siempre cargar su implementación xib, sugiero la siguiente técnica. Agregue una propiedad a su clase de vista y, en el archivo xib, establézcalo en un valor predeterminado (en Atributos de tiempo de ejecución definidos por el usuario). Ahora, después de llamar, [super initWithCoder:aDecoder];verifique el valor de la propiedad. Si es el valor predeterminado, no llame [self initializeSubviews];.

Entonces, algo como esto:

-(instancetype)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];

    if (self && self._xibProperty != 666)
    {
        //We are in the storyboard code path. Initialize from the xib.
        self = [self initializeSubviews];

        //Here, you can load properties that you wish to expose to the user to set in a storyboard; e.g.:
        //self.backgroundColor = [aDecoder decodeObjectOfClass:[UIColor class] forKey:@"backgroundColor"];
    }

    return self;
}

-(instancetype)initializeSubviews {
    id view =   [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class]) owner:self options:nil] firstObject];

    return view;
}
Leo Natan
fuente
¡Gracias @LeoNatan! Acepto esta respuesta ya que es la mejor solución al problema como se indicó originalmente. Sin embargo, tenga en cuenta que ya no es posible en Swift: he agregado algunas notas separadas sobre una posible solución en ese caso.
Ken Chatfield
@KenChatfield Me di cuenta de eso en mi subproyecto Swift y me molestó. No estoy seguro de lo que están pensando, porque sin esto, gran parte de la implementación interna de Cocoa / Cocoa Touch es imposible en Swift. Mi apuesta es que habrá algunas características dinámicas cuando realmente tengan tiempo de enfocarse en las características en lugar de en los errores. Swift no está listo en absoluto, y los peores infractores son las herramientas de desarrollo.
Leo Natan
Sí, tienes razón, Swift tiene muchas asperezas en este momento. Habiendo dicho eso, parece que asignarse directamente a uno mismo fue un truco, así que estoy contento de que el compilador ahora advierte contra tales cosas (esta verificación de tipo más estricta parece ser una de las cosas buenas de la idioma). Sin embargo, tiene razón en que hace que la vinculación de vistas personalizadas con xibs sea muy complicada y un poco insatisfactoria. ¡Esperemos que, como dices, una vez que hayan terminado de solucionar los errores, veremos algunas características más dinámicas para ayudarnos un poco más con cosas como esta!
Ken Chatfield
No es un truco, es cómo funcionan los grupos de clases. Y realmente, no hay ningún problema técnico que permita que se devuelva en su lugar la devolución de un objeto que es una subclase de la clase de devolución. Tal como está, una de las piedras angulares de Cocoa y Cocoa Touch, los grupos de clases, es imposible de implementar. Terrible. Algunos marcos, como Core Data, no se pueden implementar en Swift, lo que lo convierte en un lenguaje inútil para la mayoría de mis usos.
Leo Natan
1
De alguna manera no funcionó para mí (iOS8.1 SDK). Configuré un restoreIdentifier en el XIB en lugar de un atributo de tiempo de ejecución, de lo que funcionó. Por ejemplo, configuré "MyViewRestorationID" en el xib, que en initWithCoder: verifiqué que el! [[Self restoreIdentifier] isEqualToString: @ "MyViewRestorationID"]
ingaham
26

Tenga en cuenta que este control de calidad (como muchos) es realmente de interés histórico.

Hoy en día Durante años y años, en iOS todo es solo una vista de contenedor. Tutorial completo aquí

(De hecho, Apple finalmente agregó Storyboard References , hace algún tiempo, lo que lo hace mucho más fácil).

Aquí hay un guión gráfico típico con vistas de contenedores en todas partes. Todo es una vista de contenedor. Así es como creas aplicaciones.

ingrese la descripción de la imagen aquí

(Como curiosidad, la respuesta de KenC muestra exactamente cómo se solía hacer para cargar un xib en una especie de vista contenedora, ya que realmente no se puede "asignar a uno mismo").

Fattie
fuente
El problema con esto es que terminará con una gran cantidad de ViewControllers para todas las vistas de contenido incorporadas.
Bogdan Onu
¡Hola @BogdanOnu! Debería tener muchos, muchos, muchos controladores de vista. Para lo "más pequeño", debería tener un controlador de vista.
Fattie
2
Esto es muy útil, gracias @JoeBlow. Definitivamente, el uso de vistas de contenedor parece ser un enfoque alternativo y una forma sencilla de evitar todas las complicaciones de tratar con xibs directamente. Sin embargo, no parece una alternativa 100% satisfactoria para crear componentes reutilizables para su distribución / uso en todos los proyectos, ya que requiere que todo el diseño de la interfaz de usuario se incruste directamente en el guión gráfico.
Ken Chatfield
3
Tengo menos problemas con el uso de un ViewController adicional en este caso, ya que solo contendría la lógica del programa que de otro modo pertenecería a la clase de Vista personalizada en el caso xib, pero el estrecho acoplamiento con el guión gráfico significa que estoy No estoy seguro de que las vistas de contenedores puedan resolver este problema por completo. Quizás en la mayoría de las situaciones prácticas, las vistas son específicas del proyecto, por lo que esta es la mejor y más 'estándar' solución, pero me sorprende que todavía no haya una manera fácil de empaquetar vistas personalizadas para el uso del guión gráfico. El impulso de mi programador de dividir y conquistar en componentes aislados me pica;)
Ken Chatfield
1
Además, con la introducción de la representación en vivo de subclases de UIView personalizadas en Xcode 6, no estoy seguro de si compro la premisa de que la creación de vistas usando xibs de esta manera ahora está obsoleta
Ken Chatfield
24

Estoy agregando esto como una publicación separada para actualizar la situación con el lanzamiento de Swift. El enfoque descrito por LeoNatan funciona perfectamente en Objective-C. Sin embargo, las comprobaciones de tiempo de compilación más estrictas evitan que selfse asignen al cargar desde el archivo xib en Swift.

Como resultado, no hay otra opción que agregar la vista cargada desde el archivo xib como una subvista de la subclase UIView personalizada, en lugar de reemplazarse por completo. Esto es análogo al segundo enfoque descrito en la pregunta original. Un esquema general de una clase en Swift que utiliza este enfoque es el siguiente:

@IBDesignable // <- to optionally enable live rendering in IB
class ExampleView: UIView {

    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        initializeSubviews()
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        initializeSubviews()
    }

    func initializeSubviews() {
        // below doesn't work as returned class name is normally in project module scope
        /*let viewName = NSStringFromClass(self.classForCoder)*/
        let viewName = "ExampleView"
        let view: UIView = NSBundle.mainBundle().loadNibNamed(viewName,
                               owner: self, options: nil)[0] as! UIView
        self.addSubview(view)
        view.frame = self.bounds
    }

}

La desventaja de este enfoque es la introducción de una capa redundante adicional en la jerarquía de vistas que no existe cuando se utiliza el enfoque descrito por LeoNatan en Objective-C. Sin embargo, esto podría tomarse como un mal necesario y un producto de la forma fundamental en que se diseñan las cosas en Xcode (todavía me parece una locura que sea tan difícil vincular una clase UIView personalizada con un diseño de interfaz de usuario de una manera que funcione de manera consistente sobre ambos guiones gráficos y desde el código): reemplazar selfal por mayor en el inicializador nunca parecía una forma particularmente interpretable de hacer las cosas, aunque tener esencialmente dos clases de vista por vista tampoco parece tan bueno.

No obstante, un resultado feliz de este enfoque es que ya no necesitamos establecer la clase personalizada de la vista en nuestro archivo de clase en el constructor de interfaces para garantizar el comportamiento correcto al asignar a self, por lo que la llamada recursiva a init(coder aDecoder: NSCoder)cuando se emite loadNibNamed()se rompe (al no configurar custom class en el archivo xib, en su init(coder aDecoder: NSCoder)lugar se llamará a la versión simple de vainilla UIView en lugar de a nuestra versión personalizada).

Aunque no podemos hacer personalizaciones de clase a la vista almacenada en el xib directamente, aún podemos vincular la vista a nuestra subclase UIView 'principal' usando salidas / acciones, etc.después de configurar el propietario del archivo de la vista en nuestra clase personalizada:

Configuración de la propiedad del propietario del archivo de la vista personalizada

En el siguiente video se puede encontrar un video que demuestra la implementación de tal clase de vista paso a paso utilizando este enfoque .

Ken Chatfield
fuente
Hola Joe: gracias por tus comentarios, eso es muy atrevido (¡como en muchos de tus otros comentarios!). Como dije en respuesta a tu respuesta, estoy de acuerdo en que para la mayoría de las situaciones, las vistas de contenedores son probablemente el mejor enfoque, pero en esos casos donde las vistas deben usarse en todos los proyectos (o distribuirse), tiene sentido al menos para mí y aparentemente para otros también tener una alternativa. Puede que usted personalmente crea que es de mal estilo, pero tal vez podría dejar que las muchas otras publicaciones aquí sugiriendo cómo se podría hacer esto como referencia, y dejar que la gente juzgue por sí misma. Ambos enfoques parecen ser útiles.
Ken Chatfield
Gracias por esto. Probé varios enfoques diferentes en Swift sin éxito hasta que escuché su consejo sobre dejar la clase de plumilla como UIView. Estoy de acuerdo en que es una locura que Apple nunca haya facilitado esto, y ahora es prácticamente imposible. Un contenedor no siempre es la respuesta.
Echelon
16

PASO 1. Reemplazo selfde Storyboard

Sustitución selfen el initWithCoder:método con el error siguiente.

'NSGenericException', reason: 'This coder requires that replaced objects be returned from initWithCoder:'

En su lugar, puede reemplazar el objeto decodificado con awakeAfterUsingCoder:(not awakeFromNib). me gusta:

@implementation MyCustomView
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
    return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                          owner:nil
                                        options:nil] objectAtIndex:0];
}
@end

PASO 2. Prevención de llamadas recursivas

Por supuesto, esto también causa problemas de llamadas recursivas. (decodificación del guión gráfico -> awakeAfterUsingCoder:-> loadNibNamed:-> awakeAfterUsingCoder:-> loadNibNamed:-> ...)
Por lo tanto, debe verificar que awakeAfterUsingCoder:se llama a la corriente en el proceso de decodificación del guión gráfico o el proceso de decodificación XIB. Tienes varias formas de hacerlo:

a) Utilice privado @propertyque está configurado en NIB solamente.

@interface MyCustomView : UIView
@property (assign, nonatomic) BOOL xib
@end

y establezca "Atributos de tiempo de ejecución definidos por el usuario" solo en 'MyCustomView.xib'.

Pros:

  • Ninguna

Contras:

  • Simplemente no funciona: setXib:se llamará DESPUÉS awakeAfterUsingCoder:

b) Compruebe si selftiene subvistas

Normalmente, tiene subvistas en el xib, pero no en el guión gráfico.

- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
    if(self.subviews.count > 0) {
        // loading xib
        return self;
    }
    else {
        // loading storyboard
        return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                              owner:nil
                                            options:nil] objectAtIndex:0];
    }
}

Pros:

  • Ningún truco en Interface Builder.

Contras:

  • No puede tener subvistas en su Storyboard.

c) Establecer una bandera estática durante la loadNibNamed:llamada

static BOOL _loadingXib = NO;

- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
    if(_loadingXib) {
        // xib
        return self;
    }
    else {
        // storyboard
        _loadingXib = YES;
        typeof(self) view = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                                           owner:nil
                                                         options:nil] objectAtIndex:0];
        _loadingXib = NO;
        return view;
    }
}

Pros:

  • Sencillo
  • Ningún truco en Interface Builder.

Contras:

  • No seguro: la bandera estática compartida es peligrosa

d) Usar subclase privada en XIB

Por ejemplo, declare _NIB_MyCustomViewcomo una subclase de MyCustomView. Y utilícelo en _NIB_MyCustomViewlugar de solo MyCustomViewen su XIB.

MyCustomView.h:

@interface MyCustomView : UIView
@end

MyCustomView.m:

#import "MyCustomView.h"

@implementation MyCustomView
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
    // In Storyboard decoding path.
    return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                          owner:nil
                                        options:nil] objectAtIndex:0];
}
@end

@interface _NIB_MyCustomView : MyCustomView
@end

@implementation _NIB_MyCustomView
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
    // In XIB decoding path.
    // Block recursive call.
    return self;
}
@end

Pros:

  • No explícito ifenMyCustomView

Contras:

  • Truco _NIB_de prefijo en xib Interface Builder
  • relativamente más códigos

e) Usar subclase como marcador de posición en Storyboard

Similar a la d)subclase en Storyboard, pero con uso de la clase original en XIB.

Aquí, declaramos MyCustomViewProtocomo una subclase de MyCustomView.

@interface MyCustomViewProto : MyCustomView
@end
@implementation MyCustomViewProto
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
    // In storyboard decoding
    // Returns MyCustomView loaded from NIB.
    return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self superclass])
                                          owner:nil
                                        options:nil] objectAtIndex:0];
}
@end

Pros:

  • Muy seguro
  • Limpiar; No hay código adicional en MyCustomView.
  • Sin ifverificación explícita igual qued)

Contras:

  • Necesita usar una subclase en el guión gráfico.

Creo que e)es la estrategia más segura y limpia. Así que adoptamos eso aquí.

PASO 3. Copiar propiedades

Después, loadNibNamed:en 'awakeAfterUsingCoder:', debe copiar varias propiedades de las selfque se decodifica la instancia del Storyboard. framey las propiedades de diseño automático / tamaño automático son especialmente importantes.

- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
    typeof(self) view = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                                       owner:nil
                                                     options:nil] objectAtIndex:0];
    // copy layout properities.
    view.frame = self.frame;
    view.autoresizingMask = self.autoresizingMask;
    view.translatesAutoresizingMaskIntoConstraints = self.translatesAutoresizingMaskIntoConstraints;

    // copy autolayout constraints
    NSMutableArray *constraints = [NSMutableArray array];
    for(NSLayoutConstraint *constraint in self.constraints) {
        id firstItem = constraint.firstItem;
        id secondItem = constraint.secondItem;
        if(firstItem == self) firstItem = view;
        if(secondItem == self) secondItem = view;
        [constraints addObject:[NSLayoutConstraint constraintWithItem:firstItem
                                                            attribute:constraint.firstAttribute
                                                            relatedBy:constraint.relation
                                                               toItem:secondItem
                                                            attribute:constraint.secondAttribute
                                                           multiplier:constraint.multiplier
                                                             constant:constraint.constant]];
    }

    // move subviews
    for(UIView *subview in self.subviews) {
        [view addSubview:subview];
    }
    [view addConstraints:constraints];

    // Copy more properties you like to expose in Storyboard.

    return view;
}

SOLUCIÓN FINAL

Como puede ver, esto es un código repetitivo. Podemos implementarlos como 'categoría'. Aquí, extiendo el UIView+loadFromNibcódigo de uso común .

#import <UIKit/UIKit.h>

@interface UIView (loadFromNib)
@end

@implementation UIView (loadFromNib)

+ (id)loadFromNib {
    return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass(self)
                                          owner:nil
                                        options:nil] objectAtIndex:0];
}

- (void)copyPropertiesFromPrototype:(UIView *)proto {
    self.frame = proto.frame;
    self.autoresizingMask = proto.autoresizingMask;
    self.translatesAutoresizingMaskIntoConstraints = proto.translatesAutoresizingMaskIntoConstraints;
    NSMutableArray *constraints = [NSMutableArray array];
    for(NSLayoutConstraint *constraint in proto.constraints) {
        id firstItem = constraint.firstItem;
        id secondItem = constraint.secondItem;
        if(firstItem == proto) firstItem = self;
        if(secondItem == proto) secondItem = self;
        [constraints addObject:[NSLayoutConstraint constraintWithItem:firstItem
                                                            attribute:constraint.firstAttribute
                                                            relatedBy:constraint.relation
                                                               toItem:secondItem
                                                            attribute:constraint.secondAttribute
                                                           multiplier:constraint.multiplier
                                                             constant:constraint.constant]];
    }
    for(UIView *subview in proto.subviews) {
        [self addSubview:subview];
    }
    [self addConstraints:constraints];
}

Usando esto, puede declarar MyCustomViewProtocomo:

@interface MyCustomViewProto : MyCustomView
@end

@implementation MyCustomViewProto
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
    MyCustomView *view = [MyCustomView loadFromNib];
    [view copyPropertiesFromPrototype:self];

    // copy additional properties as you like.

    return view;
}
@end

XIB:

Captura de pantalla de XIB

Guión gráfico:

Storyboard

Resultado:

ingrese la descripción de la imagen aquí

rintaro
fuente
3
La solución es más complicada que el problema inicial. Para detener el bucle recursivo, solo necesita establecer el objeto Propietario del archivo en lugar de declarar la vista de contenido como el tipo de clase MyCustomView.
Bogdan Onu
Es solo una compensación de a) un proceso de inicialización simple pero una jerarquía de vista complicada yb) un proceso de inicialización complicado pero una jerarquía de vista simple. n'est-ce pas? ;)
rintaro
¿Hay algún enlace de descarga para este proyecto?
karthikeyan
13

No olvides

Dos puntos importantes:

  1. Establezca el propietario del archivo del .xib en el nombre de clase de su vista personalizada.
  2. No establezca el nombre de la clase personalizada en IB para la vista raíz del .xib.

Vine a esta página de preguntas y respuestas varias veces mientras aprendía a crear vistas reutilizables. Olvidar los puntos anteriores me hizo perder mucho tiempo tratando de averiguar qué estaba causando que ocurriera la recursividad infinita. Estos puntos se mencionan en otras respuestas aquí y en otros lugares , pero solo quiero volver a enfatizarlos aquí.

Mi respuesta rápida completa con pasos está aquí .

Suragch
fuente
2

Existe una solución que es mucho más limpia que las soluciones anteriores: https://www.youtube.com/watch?v=xP7YvdlnHfA

Sin propiedades de tiempo de ejecución, sin ningún problema de llamadas recursivas. Lo probé y funcionó como un encanto usando desde el guión gráfico y desde XIB con propiedades de IBOutlet (iOS8.1, XCode6).

¡Buena suerte para la codificación!

ingaham
fuente
1
¡Gracias @ingaham! Sin embargo, el enfoque descrito en el video es idéntico a la segunda solución propuesta en la pregunta original (código Swift que se presenta en mi respuesta anterior). Como en ambos casos, implica agregar una subvista a una subclase de UIView contenedora, y es por eso que no hay problemas con las llamadas recursivas, ni es necesario depender de las propiedades de tiempo de ejecución o cualquier otra cosa complicada. Como se mencionó, la desventaja es que se debe agregar una vista secundaria adicional redundante a la clase UIView personalizada. Sin embargo, como se discutió, esta puede ser la mejor y más simple solución por ahora.
Ken Chatfield
Sí, tiene toda la razón, son soluciones idénticas. Sin embargo, es necesaria una vista redundante, pero es la solución más limpia y fácil de mantener. Así que decidí usar este.
ingaham
Creo que una visión redundante es total y completamente natural, y nunca puede haber otra solución. Tenga en cuenta que está diciendo "'algo' va a ir 'aquí'" ... que "aquí" es una cosa que existe de forma natural. Simplemente tiene que haber "algo" allí, un "lugar donde colocarás las cosas", la definición misma de lo que es una "vista". ¡Y espera! Las cosas de la "vista de contenedor" de Apple son, de hecho, precisamente eso ... hay un "marco", una "vista de contenedor" (la "vista de contenedor") a la que se accede. De hecho, las soluciones de vista 'redundantes' son precisamente una vista de contenedor hecha a mano. Solo usa el de Apple.
Fattie