¿Cómo creo una clase de vista de iOS personalizada y creo varias copias de ella (en IB)?

114

Actualmente estoy creando una aplicación que tendrá varios temporizadores, que son básicamente todos iguales.

Quiero crear una clase personalizada que use todo el código para los temporizadores, así como el diseño / animaciones, de modo que pueda tener 5 temporizadores idénticos que funcionen de forma independiente entre sí.

Creé el diseño usando IB (xcode 4.2) y todo el código para los temporizadores está actualmente solo en la clase viewcontroller.

Estoy teniendo dificultades para entender cómo encapsular todo en una clase personalizada y luego agregarlo al controlador de vista, cualquier ayuda sería muy apreciada.

Michael Campsall
fuente
Para cualquiera que busque en Google aquí ... ¡ya han pasado cinco años desde que las vistas de contenedores llegan a iOS! Las vistas de contenedor son la idea básica y central de cómo se hace todo en iOS ahora. Son increíblemente fáciles de usar y hay una gran cantidad de excelentes tutoriales (¡de cinco años!) Sobre vistas de contenedores, por ejemplo , por ejemplo
Fattie
2
@JoeBlow Excepto que no puede usar una vista de contenedor en un UITableViewCello UICollectionViewCell. En mi caso, necesito una vista pequeña pero bastante compleja que pueda usar muchas veces en controladores y vistas de colección. Y los diseñadores siguen modificándolo, así que quiero un solo lugar donde se defina el diseño. Por lo tanto, una plumilla.
Echelon

Respuestas:

142

Bueno, para responder conceptualmente, su temporizador probablemente debería ser una subclase de en UIViewlugar de NSObject.

Para crear una instancia de su temporizador en IB, simplemente arrastre una UIViewgota en la vista de su controlador de vista y configure su clase con el nombre de la clase de su temporizador.

ingrese la descripción de la imagen aquí

Recuerda #import su clase de temporizador en su controlador de vista.

Editar: para diseño IB (para instanciación de código, consulte el historial de revisiones)

No estoy muy familiarizado con el guión gráfico, pero sé que puede construir su interfaz en IB usando un .xibarchivo que es casi idéntico al uso de la versión del guión gráfico; Incluso debería poder copiar y pegar sus vistas como un todo desde su interfaz existente al.xib archivo.

Para probar esto, creé un nuevo vacío .xibllamado "MyCustomTimerView.xib". Luego agregué una vista, y a eso agregué una etiqueta y dos botones. Al igual que:

ingrese la descripción de la imagen aquí

UIViewCreé una nueva subclase de clase de objetivo-C llamada "MyCustomTimer". En mi .xib, configuré la clase Propietario de mi archivo como MyCustomTimer . Ahora soy libre de conectar acciones y salidas como cualquier otra vista / controlador. El .harchivo resultante se ve así:

@interface MyCustomTimer : UIView
@property (strong, nonatomic) IBOutlet UILabel *displayLabel;
@property (strong, nonatomic) IBOutlet UIButton *startButton;
@property (strong, nonatomic) IBOutlet UIButton *stopButton;
- (IBAction)startButtonPush:(id)sender;
- (IBAction)stopButtonPush:(id)sender;
@end

El único obstáculo que queda por superar es conseguir esto .xiben mi UIViewsubclase. El uso de un .xibreduce drásticamente la configuración requerida. Y dado que está utilizando guiones gráficos para cargar los temporizadores, sabemos que -(id)initWithCoder:es el único inicializador que se llamará. Así que así es como se ve el archivo de implementación:

#import "MyCustomTimer.h"
@implementation MyCustomTimer
@synthesize displayLabel;
@synthesize startButton;
@synthesize stopButton;
-(id)initWithCoder:(NSCoder *)aDecoder{
    if ((self = [super initWithCoder:aDecoder])){
        [self addSubview:
         [[[NSBundle mainBundle] loadNibNamed:@"MyCustomTimerView" 
                                        owner:self 
                                      options:nil] objectAtIndex:0]];
    }
    return self;
}
- (IBAction)startButtonPush:(id)sender {
    self.displayLabel.backgroundColor = [UIColor greenColor];
}
- (IBAction)stopButtonPush:(id)sender {
    self.displayLabel.backgroundColor = [UIColor redColor];
}
@end

El método llamado loadNibNamed:owner:options: hace exactamente lo que parece. Carga el Nib y establece la propiedad "Propietario del archivo" en uno mismo. Extraemos el primer objeto de la matriz y esa es la vista raíz del Nib. Agregamos la vista como una subvista y listo, está en la pantalla.

Obviamente, esto solo cambia el color de fondo de la etiqueta cuando se presionan los botones, pero este ejemplo debería ayudarlo a avanzar.

Notas basadas en comentarios:

Vale la pena señalar que si tiene problemas de recursividad infinita, probablemente se haya perdido el truco sutil de esta solución. No está haciendo lo que crees que está haciendo. La vista que se coloca en el guión gráfico no se ve, sino que carga otra vista como una subvista. Esa vista que carga es la vista que se define en el nib. El "propietario del archivo" en la plumilla es esa vista invisible. Lo bueno es que esta vista invisible sigue siendo una clase Objective-C que se puede usar como una especie de controlador de vista para la vista que trae desde el nib. Por ejemplo, los IBActionmétodos de la MyCustomTimerclase son algo que esperaría más en un controlador de vista que en una vista.

Como nota al margen, algunos pueden argumentar que esto se rompe MVCy estoy algo de acuerdo. Desde mi punto de vista, está más relacionado con una costumbre UITableViewCell, que a veces también tiene que ser parte del controlador.

También vale la pena señalar que esta respuesta fue para proporcionar una solución muy específica; cree una plumilla que se pueda instanciar varias veces en la misma vista que se presenta en un guión gráfico. Por ejemplo, podría imaginarse fácilmente seis de estos temporizadores en la pantalla de un iPad a la vez. Si solo necesita especificar una vista para un controlador de vista que se usará varias veces en su aplicación, entonces la solución proporcionada por jyavenard a esta pregunta es casi con certeza una mejor solución para usted.

NJones
fuente
En realidad, esto parece una pregunta separada. Pero creo que se puede responder rápidamente, así que aquí va. Cada vez que crea una nueva notificación, es una nueva notificación, pero si necesita que tengan el mismo nombre, desea agregar algo para diferenciarlos, entonces use la userInfopropiedad en la notificación@property(nonatomic,copy) NSDictionary *userInfo;
NJones
28
Obtengo una recursividad infinita de initWithCoder. ¿Algunas ideas?
desapareció
6
Perdón por revivir un hilo antiguo, asegurándome de que File's Owneresté configurado con el nombre de tu clase resolvió mi problema de recursividad infinita.
themanatuf
20
Otra razón para la recursividad infinita es configurar la vista raíz en el xib a su clase personalizada. No quieres hacer eso, déjalo como el "UIView" predeterminado
XY
11
Solo hay una cosa que me llama la atención sobre este enfoque ... ¿No le molesta a nadie que ahora 2 Vistas estén en la misma clase? el archivo original .h / .m heredado de UIView es el primero y al hacer [self addSubview:[[[NSBundle mainBundle] loadNibNamed:@"MyCustomTimerView" owner:self options:nil] objectAtIndex:0]]; Agregar la segunda UIView ... ¿Esto parece extraño para mí, nadie más siente lo mismo? Si es así, ¿es algo que Apple hizo por diseño? ¿O alguien encontró la solución al hacer esto?
SH
137

Ejemplo rápido

Actualizado para Xcode 10 y Swift 4

Aquí hay un recorrido básico. Originalmente aprendí mucho de esto viendo esta serie de videos de Youtube . Más tarde actualicé mi respuesta basada en este artículo .

Agregar archivos de vista personalizados

Los siguientes dos archivos formarán su vista personalizada:

  • archivo .xib para contener el diseño
  • archivo .swift como UIViewsubclase

Los detalles para agregarlos se encuentran a continuación.

Archivo Xib

Agregue un archivo .xib a su proyecto (Archivo> Nuevo> Archivo ...> Interfaz de usuario> Ver) . Estoy llamando mía ReusableCustomView.xib.

Cree el diseño que desea que tenga su vista personalizada. A modo de ejemplo, voy a hacer un diseño con una UILabely una UIButton. Es una buena idea usar el diseño automático para que las cosas cambien de tamaño automáticamente sin importar el tamaño que establezca más adelante. ( Usé Freeform para el tamaño de xib en el inspector de atributos para poder ajustar las métricas simuladas, pero esto no es necesario).

ingrese la descripción de la imagen aquí

Archivo rápido

Agregue un archivo .swift a su proyecto (Archivo> Nuevo> Archivo ...> Fuente> Archivo Swift) . Es una subclase de UIViewy yo llamo mía ReusableCustomView.swift.

import UIKit
class ResuableCustomView: UIView {

}

Hacer que el archivo Swift sea el propietario

Vuelva a su archivo .xib y haga clic en "Propietario del archivo" en el esquema del documento. En el Inspector de identidad, escriba el nombre de su archivo .swift como el nombre de la clase personalizada.

ingrese la descripción de la imagen aquí

Agregar código de vista personalizado

Reemplace el ReusableCustomView.swiftcontenido del archivo con el siguiente código:

import UIKit

@IBDesignable
class ResuableCustomView: UIView {

    let nibName = "ReusableCustomView"
    var contentView:UIView?

    @IBOutlet weak var label: UILabel!

    @IBAction func buttonTap(_ sender: UIButton) {
        label.text = "Hi"
    }

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

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

    func commonInit() {
        guard let view = loadViewFromNib() else { return }
        view.frame = self.bounds
        self.addSubview(view)
        contentView = view
    }

    func loadViewFromNib() -> UIView? {
        let bundle = Bundle(for: type(of: self))
        let nib = UINib(nibName: nibName, bundle: bundle)
        return nib.instantiate(withOwner: self, options: nil).first as? UIView
    }
}

Asegúrese de escribir correctamente el nombre de su archivo .xib.

Conecta los puntos de venta y las acciones

Conecte los puntos de venta y las acciones controlando el arrastre desde la etiqueta y el botón en el diseño xib hasta el código de vista personalizada rápida.

Usa tu vista personalizada

Su vista personalizada está terminada ahora. Todo lo que tiene que hacer es agregar un UIViewlugar donde lo desee en su guión gráfico principal. Establezca el nombre de clase de la vista en ReusableCustomViewen el Inspector de identidad.

ingrese la descripción de la imagen aquí

Suragch
fuente
31
Importante no establecer la clase de vista Xib como ResuableCustomView, sino como propietario de este Xib. De lo contrario, tendrá un error.
RealNmae
5
Sí, buen recordatorio. No sé cuánto tiempo he perdido tratando de resolver problemas de recursividad infinita causados ​​por configurar la vista de Xib (en lugar de la del propietario del archivo) al nombre de la clase.
Suragch
2
@TomCalmon, rehice este proyecto en Xcode 8 con Swift 3 y Auto Layout funcionaba bien para mí.
Suragch
6
Si alguien más tiene un problema con la representación de la vista en IB, descubrí que cambiar Bundle.maina Bundle(for: ...)funciona. Aparentemente, Bundle.mainno está disponible en tiempo de diseño.
Crizzis
4
La razón por la que esto no funciona con @IBDesignable es que no lo implementó init(frame:). Una vez que agrega esto (y extrae todo el código de inicialización en alguna commonInit()función), funciona bien y se muestra en IB. Vea más información aquí: stackoverflow.com/questions/31265906/…
Dimitris
39

Respuesta para controladores de vista, no vistas :

Existe una forma más fácil de cargar un xib desde un guión gráfico. Digamos que su controlador es del tipo MyClassController que hereda de UIViewController.

Agrega un UIViewControllerIB usando en su guión gráfico; cambie el tipo de clase para que sea MyClassController. Elimine la vista que se había agregado automáticamente en el guión gráfico.

Asegúrese de que el XIB al que desea llamar se llame MyClassController.xib.

Cuando la clase se instanciará durante la carga del guión gráfico, el xib se cargará automáticamente. La razón de esto se debe a la implementación predeterminada de la UIViewControllercual llama al XIB nombrado con el nombre de la clase.

jyavenard
fuente
1
Inflo las vistas personalizadas todo el tiempo, pero nunca conocí este pequeño truco para cargarlo en el guión gráfico. ¡Muy apreciado!
MrTristan
38
La pregunta es sobre vistas, no sobre controladores de vistas.
Ricardo
Título agregado para aclarar, "Respuesta para controladores de vista, no vistas:"
nacho4d
1
No parece funcionar en iOS 9.1. Si elimino la vista creada automáticamente (predeterminada), se bloquea después de regresar de viewDidLoad del VC. No creo que la vista en el XIB se conecte en lugar de la vista predeterminada.
Jeff
1
La excepción que obtengo es 'Could not load NIB in bundle: 'NSBundle </Users/me/.../MyAppName.app> (loaded)' with name 'uB0-aR-WjG-view-4OA-9v-tyV''. Tenga en cuenta el whacko nibName. Estoy usando el nombre de clase correcto para el en la plumilla.
Jeff
16

Esto no es realmente una respuesta, pero creo que es útil compartir este enfoque.

C objetivo

  1. Importación CustomViewWithXib.h y CustomViewWithXib.m a su proyecto
  2. Cree los archivos de vista personalizada con el mismo nombre (.h / .m / .xib)
  3. Herede su clase personalizada de CustomViewWithXib

Rápido

  1. Importe CustomViewWithXib.swift a su proyecto
  2. Cree los archivos de vista personalizada con el mismo nombre (.swift y .xib)
  3. Herede su clase personalizada de CustomViewWithXib

Opcional :

  1. Vaya a su archivo xib, establezca el propietario con su nombre de clase personalizado si necesita conectar algunos elementos (para obtener más detalles, consulte la parte Hacer que el archivo Swift sea el propietario de las respuestas de @Suragch)

Es todo, ahora puede agregar su vista personalizada en su guión gráfico y se mostrará :)

CustomViewWithXib.h :

 #import <UIKit/UIKit.h>

/**
 *  All classes inherit from CustomViewWithXib should have the same xib file name and class name (.h and .m)
 MyCustomView.h
 MyCustomView.m
 MyCustomView.xib
 */

// This allows seeing how your custom views will appear without building and running your app after each change.
IB_DESIGNABLE
@interface CustomViewWithXib : UIView

@end

CustomViewWithXib.m :

#import "CustomViewWithXib.h"

@implementation CustomViewWithXib

#pragma mark - init methods

- (instancetype)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        // load view frame XIB
        [self commonSetup];
    }
    return self;
}

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];
    if (self) {
        // load view frame XIB
        [self commonSetup];
    }
    return self;
}

#pragma mark - setup view

- (UIView *)loadViewFromNib {
    NSBundle *bundle = [NSBundle bundleForClass:[self class]];

    //  An exception will be thrown if the xib file with this class name not found,
    UIView *view = [[bundle loadNibNamed:NSStringFromClass([self class])  owner:self options:nil] firstObject];
    return view;
}

- (void)commonSetup {
    UIView *nibView = [self loadViewFromNib];
    nibView.frame = self.bounds;
    // the autoresizingMask will be converted to constraints, the frame will match the parent view frame
    nibView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    // Adding nibView on the top of our view
    [self addSubview:nibView];
}

@end

CustomViewWithXib.swift :

import UIKit

@IBDesignable
class CustomViewWithXib: UIView {

    // MARK: init methods
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)

        commonSetup()
    }

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

        commonSetup()
    }

    // MARK: setup view 

    private func loadViewFromNib() -> UIView {
        let viewBundle = NSBundle(forClass: self.dynamicType)
        //  An exception will be thrown if the xib file with this class name not found,
        let view = viewBundle.loadNibNamed(String(self.dynamicType), owner: self, options: nil)[0]
        return view as! UIView
    }

    private func commonSetup() {
        let nibView = loadViewFromNib()
        nibView.frame = bounds
        // the autoresizingMask will be converted to constraints, the frame will match the parent view frame
        nibView.autoresizingMask = [.FlexibleWidth, .FlexibleHeight]
        // Adding nibView on the top of our view
        addSubview(nibView)
    }
}

Puedes encontrar algunos ejemplos aquí .

Espero que ayude.

HamzaGhazouani
fuente
entonces, ¿cómo podemos agregar iboutlet para la vista uicontrols
Amr Angry
1
Hola @AmrAngry, para conectar las vistas debe hacer el paso 4: Vaya a su archivo xib, en "Propietario del archivo" configure su clase, y arrastre y suelte sus vistas en la interfaz tocando el botón "ctrl", actualizo el código fuente con este ejemplo, no lo dude si tiene alguna pregunta, espero que ayude
HamzaGhazouani
muchas gracias por tu repetición, ya hago esto pero supongo que mi problema no está en esta parte. mi problema es que agrego un UIDatePIcker e intento agregar una acción de destino para este control desde ViewController para configurar y el evento uiControleer cambia de valor, pero el método nunca se llama / fire
Amr Angry
1
@AmrAngry Actualizo el ejemplo agregando la vista del selector, puede que te ayude :) github.com/HamzaGhazouani/Stackoverflow/tree/master/…
HamzaGhazouani
1
Gracias, estuve buscando esto durante mucho tiempo.
MH175