De acuerdo, hay docenas de publicaciones en StackOverflow sobre esto, pero ninguna es particularmente clara sobre la solución. Me gustaría crear un UIView
archivo 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:
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; }
Crear una instancia mediante programación usando
-(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 [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:
- Coloque una
UIView
vista principal en el guión gráfico - Establezca su clase personalizada en
MyCustomView
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)initializeSubviews
y 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 UIView
con 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?
fuente
Respuestas:
Tu problema es llamar
loadNibNamed:
de (un descendiente de)initWithCoder:
.loadNibNamed:
llamadas internasinitWithCoder:
. 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; }
fuente
Tenga en cuenta que este control de calidad (como muchos) es realmente de interés histórico.
Hoy en díaDurante 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.
(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").
fuente
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
self
se 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
self
al 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 ainit(coder aDecoder: NSCoder)
cuando se emiteloadNibNamed()
se rompe (al no configurar custom class en el archivo xib, en suinit(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:
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 .
fuente
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.PASO 1. Reemplazo
self
de StoryboardSustitución
self
en elinitWithCoder:
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:
(notawakeFromNib
). 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
@property
que 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:
Contras:
setXib:
se llamará DESPUÉSawakeAfterUsingCoder:
b) Compruebe si
self
tiene subvistasNormalmente, 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:
Contras:
c) Establecer una bandera estática durante la
loadNibNamed:
llamadastatic 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:
Contras:
d) Usar subclase privada en XIB
Por ejemplo, declare
_NIB_MyCustomView
como una subclase deMyCustomView
. Y utilícelo en_NIB_MyCustomView
lugar de soloMyCustomView
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:
if
enMyCustomView
Contras:
_NIB_
de prefijo en xib Interface Buildere) 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
MyCustomViewProto
como una subclase deMyCustomView
.@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:
MyCustomView
.if
verificación explícita igual qued)
Contras:
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 lasself
que se decodifica la instancia del Storyboard.frame
y 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+loadFromNib
có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
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:
Guión gráfico:
Resultado:
fuente
No olvides
Dos puntos importantes:
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í .
fuente
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!
fuente