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

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

  • No UIViewController separado: una clase completamente autónoma
  • Outlets en la clase que me permiten 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. Cree una instancia mediante progtwigción mediante -(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 a [super init] y simplemente configurar el objeto usando el contenido del plumín cargado parece un poco sospechoso; aquí hay consejos para agregar una subvista que también funciona bien). Sin embargo, me gustaría poder crear una instancia de la vista desde el guión gráfico también. Así que puedo:

  1. Coloque una UIView en una vista principal en el guión gráfico
  2. Establezca su clase personalizada en MyCustomView
  3. Override -(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 sea si utilizo el enfoque anterior, o si hago una instancia programática, ambos terminan llamando recursivamente -(id)initWithCoder: al ingresar -(void)initializeSubviews y se carga el plumín desde el archivo.

Varias otras preguntas SO se ocupan de esto, como aquí , aquí , aquí y aquí . Sin embargo, ninguna de las respuestas dadas resuelve satisfactoriamente el problema:

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

¿Podría alguien dar consejos sobre cómo resolver este problema, y ​​obtener salidas de trabajo en una UIView personalizada con mínimo alboroto / sin envoltorio de controlador delgado? ¿O hay una forma alternativa y más limpia de hacer las cosas con un código mínimo estándar?

Su problema es llamar a loadNibNamed: from (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, le sugiero la siguiente técnica. Agregue una propiedad a su clase de vista, y en el archivo xib, configúrelo en un valor predeterminado (en Atributos de tiempo de ejecución definidos por el usuario). Ahora, después de llamar a [super initWithCoder:aDecoder]; verifica 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; eg: //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; } 

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 ahora en iOS todo es solo una vista de contenedor. Tutorial completo aquí

(De hecho, Apple finalmente agregó las referencias a Storyboard , hace algún tiempo, haciéndolo 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. Es solo cómo haces aplicaciones.

enter image description here

(Como curiosidad, la respuesta de KenC muestra exactamente cómo, solía hacerse para cargar un xib a una especie de vista de contenedor, ya que realmente no se puede “asignar a sí mismo”).

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 comstackción más estrictas impiden que self asigne uno al cargar desde el archivo xib en Swift.

Como resultado, no hay más opción que agregar la vista cargada desde el archivo xib como una subvista de la subclase UIView personalizada, en lugar de reemplazarla por completo. Esto es análogo al segundo enfoque esbozado en la pregunta original. Un esquema aproximado de una clase en Swift usando 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 manera 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 UI de una manera que funcione de manera consistente tanto en los guiones gráficos como en el código): reemplazar el self al por mayor en el inicializador antes nunca parecía una forma particularmente interpretable de hacer las cosas, aunque tener esencialmente dos clases de vista por visión tampoco parece tan bueno.

No obstante, un resultado feliz de este enfoque es que ya no es necesario configurar la clase personalizada de la vista en nuestro archivo de clase en el constructor de interfaz para garantizar un comportamiento correcto cuando se asigna a self , por lo que la llamada recursiva a init(coder aDecoder: NSCoder) cuando La emisión de loadNibNamed() se interrumpe (al no configurar la clase personalizada en el archivo xib, se llamará a init(coder aDecoder: NSCoder) de plain vanilla UIView en lugar de a nuestra versión personalizada).

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

Establecer 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 dicha clase de vista paso a paso con este enfoque.

PASO 1. Reemplazar a self de Storyboard

Sustitución de self en initWithCoder: método fallará con el siguiente error.

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

En cambio, puede reemplazar el objeto decodificado con awakeAfterUsingCoder: (no 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. Previniendo la llamada recursiva

Por supuesto, esto también causa un problema de llamada recursiva. (Desencoding del guión gráfico -> awakeAfterUsingCoder: -> loadNibNamed: -> awakeAfterUsingCoder: -> loadNibNamed: -> …)
Por lo tanto, debe verificar el awakeAfterUsingCoder: actual awakeAfterUsingCoder: se llama en el proceso de desencoding Storyboard o en el proceso de deencoding XIB. Tienes varias formas de hacerlo:

a) Use @property privada que está configurada en NIB solamente.

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

y establecer “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 de awakeAfterUsingCoder:

b) Compruebe si self tiene alguna sub-vista

Normalmente, tienes 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:

  • No hay truco en Interface Builder.

Contras:

  • No puedes tener subvistas en tu Storyboard.

c) Establezca un indicador estático durante loadNibNamed: call

 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
  • No hay truco en Interface Builder.

Contras:

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

d) Usar subclase privada en XIB

Por ejemplo, declare _NIB_MyCustomView como una subclase de MyCustomView . Y use _NIB_MyCustomView lugar de MyCustomView en 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 if en MyCustomView

Contras:

  • Prefijo _NIB_ truco en xib Interface Builder
  • relativamente más códigos

e) Usa la subclase como marcador de posición en Storyboard

Similar a d) pero usa la subclase en Storyboard, clase original en XIB.

Aquí, declaramos MyCustomViewProto como 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; Sin código adicional en MyCustomView .
  • No explícita if marca lo mismo que d)

Contras:

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

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

PASO 3. Copiar propiedades

Después de loadNibNamed: en ‘awakeAfterUsingCoder:’, debe copiar varias propiedades de self que se decodifica instancia f Storyboard. frame propiedades frame y autolayout / autoresize 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, este es un poco de código repetitivo. Podemos implementarlos como ‘categoría’. Aquí UIView+loadFromNib código UIView+loadFromNib uso UIView+loadFromNib .

 #import  @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, puedes declarar MyCustomViewProto como:

 @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 XIB

Storyboard:

Storyboard

Resultado:

enter image description here

No lo olvides

Dos puntos importantes:

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

Llegué a esta página de preguntas y respuestas varias veces mientras aprendía a hacer vistas reutilizables. Olvidar los puntos anteriores me hizo perder mucho tiempo tratando de descubrir qué causaba la recursión infinita. Estos puntos se mencionan en otras respuestas aquí y en otros lugares , pero solo quiero enfatizarlos aquí.

Mi respuesta Swift completa con los pasos está aquí .

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

No hay propiedades de tiempo de ejecución, ningún problema de llamada recursiva. Lo probé y funcionó como un encanto usando del guión gráfico y de XIB con las propiedades de IBOutlet (iOS8.1, XCode6).

¡Buena suerte para codificar!