ios - custom - Cargar vista desde un archivo xib externo en el guión gráfico
swift xib (7)
Actualmente, la mejor solución es simplemente usar un controlador de vista personalizado con su vista definida en un xib, y simplemente eliminar la propiedad "ver" que xcode crea dentro del guión gráfico cuando se le agrega el vc (no olvides establecer el nombre del clase personalizada sin embargo).
Esto hará que el motor de ejecución busque automáticamente el xib y lo cargue. Puede utilizar este truco para cualquier tipo de vista de contenedor o vista de contenido.
Quiero utilizar una vista a través de múltiples viewcontrollers en un guión gráfico. Por lo tanto, pensé en diseñar la vista en un xib externo para que los cambios se reflejen en cada viewcontroller. Pero, ¿cómo se puede cargar una vista desde un xib externo en un guión gráfico y aún es posible? Si ese no es el caso, ¿qué otras alternativas están disponibles para adaptarse a la situación?
Durante un tiempo, el enfoque de Christopher Swasey fue el mejor enfoque que había encontrado. ¡Le pregunté a un par de desarrolladores senior sobre mi equipo y uno de ellos tuvo la solución perfecta ! Satisface cada una de las inquietudes que Christopher Swasey trató tan elocuentemente y no requiere un código de subclase repetitivo (mi principal preocupación con su enfoque). Hay una gotcha , pero aparte de eso es bastante intuitiva y fácil de implementar.
- Cree una clase UIView personalizada en un archivo .swift para controlar su xib. es decir
MyCustomClass.swift
- Crea un archivo .xib y ajústalo como quieras. es decir
MyCustomClass.xib
- Establezca el
File''s Owner
del archivo .xib del archivo para que sea su clase personalizada (MyCustomClass
) - GOTCHA: deje en blanco el valor de la
class
(debajo delidentity Inspector
) para su vista personalizada en el archivo .xib. Por lo tanto, su vista personalizada no tendrá una clase especificada, pero tendrá un Propietario de archivo especificado. - Conecte sus puntos de venta como lo haría normalmente con el
Assistant Editor
.- NOTA: Si observa el
Connections Inspector
, notará que sus Puntos de Referencia no hacen referencia a su clase personalizada (es decir,MyCustomClass
), sino que hacen referenciaFile''s Owner
. ComoFile''s Owner
se especifica como su clase personalizada, los puntos de venta se conectarán y funcionarán correctamente.
- NOTA: Si observa el
- Asegúrese de que su clase personalizada tenga @IBDesignable antes de la declaración de clase.
- Haga que su clase personalizada se ajuste al protocolo
NibLoadable
mencionado a continuación.- NOTA: Si su nombre de archivo
.swift
clase personalizada es diferente de su nombre de archivo.xib
, configure la propiedadnibName
para que sea el nombre de su archivo.xib
.
- NOTA: Si su nombre de archivo
- Implemente el
required init?(coder aDecoder: NSCoder)
yoverride init(frame: CGRect)
para llamar asetupFromNib()
como en el ejemplo siguiente. - Agregue una UIView a su guión gráfico deseado y configure la clase como su nombre de clase personalizado (es decir,
MyCustomClass
). - Mire IBDesignable en acción mientras dibuja su .xib en el guión gráfico con todo su asombro y asombro.
Aquí está el protocolo que querrá hacer referencia:
public protocol NibLoadable {
static var nibName: String { get }
}
public extension NibLoadable where Self: UIView {
public static var nibName: String {
return String(describing: Self.self) // defaults to the name of the class implementing this protocol.
}
public static var nib: UINib {
let bundle = Bundle(for: Self.self)
return UINib(nibName: Self.nibName, bundle: bundle)
}
func setupFromNib() {
guard let view = Self.nib.instantiate(withOwner: self, options: nil).first as? UIView else { fatalError("Error loading /(self) from nib") }
addSubview(view)
view.translatesAutoresizingMaskIntoConstraints = false
view.leadingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.leadingAnchor, constant: 0).isActive = true
view.topAnchor.constraint(equalTo: self.safeAreaLayoutGuide.topAnchor, constant: 0).isActive = true
view.trailingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.trailingAnchor, constant: 0).isActive = true
view.bottomAnchor.constraint(equalTo: self.safeAreaLayoutGuide.bottomAnchor, constant: 0).isActive = true
}
}
Y aquí hay un ejemplo de MyCustomClass
que implementa el protocolo (con el archivo MyCustomClass.xib
llamado MyCustomClass.xib
):
@IBDesignable
class MyCustomClass: UIView, NibLoadable {
@IBOutlet weak var myLabel: UILabel!
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupFromNib()
}
override init(frame: CGRect) {
super.init(frame: frame)
setupFromNib()
}
}
NOTA: Si pierde Gotcha y establece el valor de class
dentro de su archivo .xib para que sea su clase personalizada, no dibujará en el guión gráfico y obtendrá un error EXC_BAD_ACCESS
cuando ejecute la aplicación porque se queda atascada en un infinito bucle de intentar inicializar la clase desde la punta utilizando el método init?(coder aDecoder: NSCoder)
que luego llama a Self.nib.instantiate
y llama de nuevo a init
.
Esta solución se puede usar incluso si su clase no tiene el mismo nombre que el XIB. Por ejemplo, si tiene un controlador de clase de controlador de vista base A que tiene un controlador de nombre XIBA.xib y ha subclasificado esto con el controlador B y desea crear una instancia de controladorB en un guión gráfico, puede:
- crear el controlador de vista en el guión gráfico
- establecer la clase del controlador en el controlador B
- eliminar la vista del controlador B en el guión gráfico
- anula la vista de carga en controllerA para:
*
- (void) loadView
{
//according to the documentation, if a nibName was passed in initWithNibName or
//this controller was created from a storyboard (and the controller has a view), then nibname will be set
//else it will be nil
if (self.nibName)
{
//a nib was specified, respect that
[super loadView];
}
else
{
//if no nib name, first try a nib which would have the same name as the class
//if that fails, force to load from the base class nib
//this is convenient for including a subclass of this controller
//in a storyboard
NSString *className = NSStringFromClass([self class]);
NSString *pathToNIB = [[NSBundle bundleForClass:[self class]] pathForResource: className ofType:@"nib"];
UINib *nib ;
if (pathToNIB)
{
nib = [UINib nibWithNibName: className bundle: [NSBundle bundleForClass:[self class]]];
}
else
{
//force to load from nib so that all subclass will have the correct xib
//this is convenient for including a subclass
//in a storyboard
nib = [UINib nibWithNibName: @"baseControllerXIB" bundle:[NSBundle bundleForClass:[self class]]];
}
self.view = [[nib instantiateWithOwner:self options:nil] objectAtIndex:0];
}
}
Mi ejemplo completo está here , pero proporcionaré un resumen a continuación.
Diseño
Agregue un archivo .swift y .xib con el mismo nombre para su proyecto. El archivo .xib contiene su diseño de vista personalizado (preferiblemente utilizando restricciones de diseño automático).
Convierta el archivo rápido en el propietario del archivo xib.
Agregue el siguiente código al archivo .swift y conecte las salidas y acciones del archivo .xib.
import UIKit
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)
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
}
}
Úselo
Use su vista personalizada en cualquier parte de su guión gráfico. Simplemente agregue un UIView
y establezca el nombre de la clase a su nombre de clase personalizado.
Pienso en una alternative
para usar XIB views
para usar View Controller
en un guión gráfico separado .
Luego, en el guión gráfico principal en lugar de la vista personalizada, use la vista de container view
con Embed Segue
y cuente con StoryboardReference
para este controlador de vista personalizado, que vista debe colocarse dentro de otra vista en el guión gráfico principal.
Luego, podemos configurar la delegación y la comunicación entre este ViewController incorporado y el controlador de vista principal a través de prepare for segue . Este enfoque es diferente a la visualización de UIView, pero mucho más simple y más eficiente (desde la perspectiva de programación) se puede utilizar para lograr el mismo objetivo, es decir, tener una vista personalizada reutilizable que sea visible en el guión gráfico principal.
La ventaja adicional es que puede implementar su lógica en la clase CustomViewController y configurar todas las delegaciones y ver la preparación sin crear clases de controlador separadas (más difíciles de encontrar en el proyecto), y sin colocar código repetitivo en UIViewController principal utilizando Component. Creo que esto es bueno para componentes reutilizables, ex. Componente de reproductor de música (parecido a un widget) que se puede integrar en otras vistas.
Siempre he encontrado insatisfactoria la solución de "agregar como una subvista", dado que está atornillada con (1) ajuste automático, (2) @IBInspectable
y (3) salidas. En cambio, déjame presentarte la magia de awakeAfter:
un método NSObject
.
awakeAfter
permite intercambiar el objeto realmente despertado desde un NIB / Storyboard con un objeto completamente diferente. A continuación, ese objeto se somete al proceso de hidratación, se lo awakeFromNib
, se lo agrega, se agrega como una vista, etc.
Podemos usar esto en una subclase de "corte de cartón" de nuestra vista, cuyo único propósito será cargar la vista desde el NIB y devolverla para su uso en el Storyboard. La subclase incrustable se especifica luego en el inspector de identidad de la vista Storyboard, en lugar de la clase original. En realidad, no tiene que ser una subclase para que esto funcione, pero convertirla en una subclase es lo que le permite a IB ver cualquier propiedad IBInspectable / IBOutlet.
Nota: la clase establecida en la vista en el archivo NIB sigue siendo la misma. La subclase incrustable solo se usa en el guión gráfico. La subclase no se puede usar para crear una instancia de la vista en el código, por lo que no debería tener ninguna lógica adicional. Solo debe contener el awakeAfter
hook.
class MyCustomEmbeddableView: MyCustomView {
override func awakeAfter(using aDecoder: NSCoder) -> Any? {
return (UIView.instantiateViewFromNib("MyCustomView") as MyCustomView?)! as Any
}
}
The️ El único inconveniente importante aquí es que si define restricciones de ancho, altura o relación de aspecto en el guión gráfico que no se relacionan con otra vista, entonces deben copiarse manualmente. Las restricciones que relacionan dos vistas se instalan en el ancestro común más cercano, y las vistas se despiertan del guión gráfico desde adentro hacia afuera, de modo que cuando esas restricciones se hidratan en la supervista, el intercambio ya se ha producido. Las restricciones que solo involucran la vista en cuestión se instalan directamente en esa vista y, por lo tanto, se lanzan cuando ocurre el intercambio a menos que se copien.
Tenga en cuenta que lo que está sucediendo aquí son las restricciones instaladas en la vista del guión gráfico que se copian en la vista recién instanciada , que puede tener restricciones propias, definidas en su archivo nib. Esos no se ven afectados.
class MyCustomEmbeddableView: MyCustomView {
override func awakeAfter(using aDecoder: NSCoder) -> Any? {
let newView = (UIView.instantiateViewFromNib("MyCustomView") as MyCustomView?)!
for constraint in constraints {
if constraint.secondItem != nil {
newView.addConstraint(NSLayoutConstraint(item: newView, attribute: constraint.firstAttribute, relatedBy: constraint.relation, toItem: newView, attribute: constraint.secondAttribute, multiplier: constraint.multiplier, constant: constraint.constant))
} else {
newView.addConstraint(NSLayoutConstraint(item: newView, attribute: constraint.firstAttribute, relatedBy: constraint.relation, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: constraint.constant))
}
}
return newView as Any
}
}
instantiateViewFromNib
es una extensión de tipo seguro para UIView
. Todo lo que hace es recorrer los objetos del NIB hasta que encuentra uno que coincida con el tipo. Tenga en cuenta que el tipo genérico es el valor de retorno , por lo que el tipo debe especificarse en el sitio de la llamada.
extension UIView {
public class func instantiateViewFromNib<T>(_ nibName: String, inBundle bundle: Bundle = Bundle.main) -> T? {
if let objects = bundle.loadNibNamed(nibName, owner: nil) {
for object in objects {
if let object = object as? T {
return object
}
}
}
return nil
}
}
Suponiendo que ha creado un xib que desea usar:
1) Cree una subclase personalizada de UIView (puede ir a Archivo -> Nuevo -> Archivo ... -> Clase Cocoa Touch. Asegúrese de que "Subclase de:" sea "UIView").
2) Agregue una vista que esté basada en el xib como una subvista a esta vista en la inicialización.
En Obj-C
-(id)initWithCoder:(NSCoder *)aDecoder{
if (self = [super initWithCoder:aDecoder]) {
UIView *xibView = [[[NSBundle mainBundle] loadNibNamed:@"YourXIBFilename"
owner:self
options:nil] objectAtIndex:0];
xibView.frame = self.bounds;
xibView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
[self addSubview: xibView];
}
return self;
}
En Swift 2
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
let xibView = NSBundle.mainBundle().loadNibNamed("YourXIBFilename", owner: self, options: nil)[0] as! UIView
xibView.frame = self.bounds
xibView.autoresizingMask = [.FlexibleWidth, .FlexibleHeight]
self.addSubview(xibView)
}
En Swift 3
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
let xibView = Bundle.main.loadNibNamed("YourXIBFilename", owner: self, options: nil)!.first as! UIView
xibView.frame = self.bounds
xibView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.addSubview(xibView)
}
3) Donde quiera que quiera usarlo en su guión gráfico, agregue un UIView como lo haría normalmente, seleccione la vista recién agregada, vaya al Inspector de identidad (el tercer ícono en la esquina superior derecha que se ve como un rectángulo con líneas). e ingrese el nombre de su subclase como "Clase" en "Clase personalizada".