ios - custom - uiview from xib swift
Crear una UIView reutilizable con xib(y cargar desde el guión gráfico) (6)
No lo olvides
Dos puntos importantes:
- Establezca el propietario del archivo .xib en el nombre de la clase de su vista personalizada.
- 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 elsewhere , pero solo quiero enfatizarlos aquí.
Mi respuesta Swift completa con los pasos está here .
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:
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; }
Cree una instancia mediante programación mediante
-(id)initWithFrame:
en mi controlador de vistaMyCustomView *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:
- Coloque una
UIView
en una vista principal en el guión gráfico - Establezca su clase personalizada en
MyCustomView
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 here , here , here y here . 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?
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. (Descodificación del guión gráfico -> awakeAfterUsingCoder:
-> loadNibNamed:
-> awakeAfterUsingCoder:
-> loadNibNamed:
-> ...)
Por lo tanto, debe verificar el awakeAfterUsingCoder:
actual awakeAfterUsingCoder:
se llama en el proceso de descodificación Storyboard o en el proceso de decodificación 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 deawakeAfterUsingCoder:
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
enMyCustomView
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 qued)
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 <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, 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:
Storyboard:
Resultado:
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 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:
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.
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!
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; 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;
}
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 developer.apple.com/library/ios/recipes/… , 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.
(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").