Cargue la vista desde un archivo xib externo en el guión gráfico


131

Quiero usar una vista en varios controles de vista 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 controlador de vista. Pero, ¿cómo se puede cargar una vista desde un xib externo en un guión gráfico e incluso es posible? Si ese no es el caso, ¿qué otras alternativas están disponibles para adaptarse a la situación anterior?



Respuestas:


132

Mi ejemplo completo está aquí , pero proporcionaré un resumen a continuación.

Diseño

Agregue un archivo .swift y .xib cada uno con el mismo nombre a su proyecto. El archivo .xib contiene su diseño de vista personalizado (preferiblemente utilizando restricciones de diseño automático).

Convierta el archivo swift en el propietario del archivo xib.

ingrese la descripción de la imagen aquí Código

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
    }
}

Úsalo

Use su vista personalizada en cualquier parte de su guión gráfico. Simplemente agregue ay UIViewconfigure el nombre de la clase a su nombre de clase personalizado.

ingrese la descripción de la imagen aquí


3
¿No es que loadNibNamed llama a init (codificador :)? Tengo un accidente tratando de adaptar tu enfoque.
Fishman

@Fishman, si intenta cargar la vista mediante programación (en lugar de desde el guión gráfico), se bloqueará porque actualmente no tiene un init(frame:). Vea este tutorial para más detalles.
Suragch

77
Otra causa común de fallas es no configurar la vista personalizada para el propietario del archivo . Ver el círculo rojo en mi respuesta.
Suragch

55
Sí, había establecido la clase de la vista raíz en lugar del propietario del archivo y estaba causando un bucle infinito.
devios1

2
Después de agregar view.autoresizingMask = [.flexibleWidth, .flexibleHeight] línea antes de self.addSubview (view) está funcionando perfectamente.
Pramod Tapaniya

69

Durante un tiempo, el enfoque de Christopher Swasey fue el mejor enfoque que había encontrado. Le pregunté a un par de desarrolladores senior de mi equipo y uno de ellos tenía la solución perfecta . Satisface todas las preocupaciones que Christopher Swasey abordó con tanta elocuencia y no requiere un código de subclase repetitivo (mi principal preocupación con su enfoque). Hay un problema , pero aparte de eso, es bastante intuitivo y fácil de implementar.

  1. Cree una clase UIView personalizada en un archivo .swift para controlar su xib. es decirMyCustomClass.swift
  2. Crea un archivo .xib y dale el estilo que quieras. es decirMyCustomClass.xib
  3. Configure el File's Ownerarchivo .xib para que sea su clase personalizada ( MyCustomClass)
  4. GOTCHA: deje en blanco el classvalor (debajo de identity Inspector) para su vista personalizada en el archivo .xib. Por lo tanto, su vista personalizada no tendrá una clase específica, pero tendrá un Propietario de archivo específico.
  5. Conecte sus enchufes como lo haría normalmente con Assistant Editor.
    • NOTA: Si observa el Connections Inspector, notará que sus salidas de referencia no hacen referencia a su clase personalizada (es decir MyCustomClass), sino que hacen referencia a ella File's Owner. Como File's Ownerse especifica que es su clase personalizada, las salidas se conectarán y funcionarán correctamente.
  6. Asegúrese de que su clase personalizada tenga @IBDesignable antes de la instrucción de clase.
  7. Haga que su clase personalizada se ajuste al NibLoadableprotocolo al que se hace referencia a continuación.
    • NOTA: Si el .swiftnombre de su .xibarchivo de clase personalizado es diferente del nombre de su archivo, configure la nibNamepropiedad para que sea el nombre de su .xibarchivo.
  8. Implemente required init?(coder aDecoder: NSCoder)y override init(frame: CGRect)llame setupFromNib()como en el siguiente ejemplo.
  9. Agregue una vista UIView al guión gráfico deseado y configure la clase para que sea su nombre de clase personalizado (es decir MyCustomClass).
  10. Mira IBDesignable en acción mientras dibuja tu .xib en el guión gráfico con todo su asombro y asombro.

Aquí está el protocolo al 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 MyCustomClassque implementa el protocolo (con el nombre del archivo .xib 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 el Gotcha y establece el classvalor dentro de su archivo .xib para que sea su clase personalizada, entonces no se dibujará en el guión gráfico y obtendrá un EXC_BAD_ACCESSerror cuando ejecute la aplicación porque se atasca en un bucle infinito de tratando de inicializar la clase desde la punta usando el init?(coder aDecoder: NSCoder)método que luego llama Self.nib.instantiatey initvuelve a llamar .


2
Aquí hay otro gran enfoque, pero creo que el anterior es aún mejor: medium.com/zenchef-tech-and-product/…
Ben Patch

44
El enfoque mencionado anteriormente funciona perfectamente y aplica una vista previa en vivo directamente en el guión gráfico. ¡Es absolutamente práctico e increíble, grande!
Igor Leonovich

1
FYI: esta solución, usando la definición de restricción en setupFromNib(), parece solucionar ciertos problemas extraños de diseño automático con celdas de vista de tabla de tamaño automático que contienen vistas creadas por XIB.
Gary

1
De lejos, la mejor solución! Amo la @IBDesignablecompatibilidad. No puedo creer por qué Xcode o UIKit no proporcionan algo como esto por defecto al agregar un archivo UIView.
fl034

1
parece que no funciona. Cada vez que configuro File Owner dentro de mi archivo .xib, TAMBIÉN establece una clase personalizada.
drewster

32

Suponiendo que ha creado una xib que desea usar:

1) Cree una subclase personalizada de UIView (puede ir a Archivo -> Nuevo -> Archivo ... -> Clase de Cocoa Touch. Asegúrese de que "Subclase de:" es "UIView").

2) Agregue una vista basada en 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 lo use en su guión gráfico, agregue una vista UIV como lo haría normalmente, seleccione la vista recién agregada, vaya al Inspector de identidad (el tercer icono en la esquina superior derecha que parece un rectángulo con líneas), e ingrese el nombre de su subclase como "Clase" en "Clase personalizada".


xibView.frame = self.frame;debería ser xibView.frame = CGRectMake(0, 0, self.frame.size.width, self.frame.size.height);, de lo contrario, xibView tendrá un desplazamiento cuando la vista se agregue al guión gráfico.
BabyPanda

tarde a la fiesta, pero parece que lo cambió a xibView.frame = self.bounds, que es un marco sin desplazamiento
Heavy_Bullets

20
Resulta en un bloqueo debido a la recursión infinita. Al cargar la punta se crea otra instancia de la subclase.
David

2
La clase de la vista xib no debería ser la misma que esta nueva subclase. Si el xib es MyClass, puede hacer que esta nueva clase MyClassContainer.
user1021430

la solución anterior se bloqueará a una recursión infinita.
JBarros35

27

Siempre he encontrado que la solución "agregarlo como una subvista" no es satisfactoria, ya que se atornilla con (1) salida automática, (2) @IBInspectabley (3) salidas. En cambio, déjame presentarte la magia de awakeAfter:un NSObjectmétodo.

awakeAfterle permite cambiar el objeto realmente despertado de un NIB / Storyboard con un objeto completamente diferente. Ese objeto luego se somete al proceso de hidratación, lo invoca awakeFromNib, se agrega como una vista, etc.

Podemos usar esto en una subclase de "recorte de cartón" de nuestra vista, cuyo único propósito será cargar la vista desde el NIB y devolverla para usarla en el Storyboard. La subclase incorporable se especifica en el inspector de identidad de la vista del Guión gráfico, en lugar de la clase original. En realidad, no tiene que ser una subclase para que esto funcione, pero convertirlo en una subclase es lo que le permite a IB ver las propiedades de IBInspectable / IBOutlet.

Esta placa adicional podría parecer subóptima, y ​​en cierto sentido lo es, porque lo ideal UIStoryboardsería manejar esto sin problemas, pero tiene la ventaja de dejar el NIB original yUIView subclase sin modificar por completo. El papel que desempeña es básicamente el de un adaptador o una clase de puente, y es perfectamente válido, en cuanto al diseño, como una clase adicional, incluso si es lamentable. Por otro lado, si prefiere ser parsimonioso con sus clases, la solución de @BenPatch funciona mediante la implementación de un protocolo con algunos otros cambios menores. La cuestión de qué solución es mejor se reduce a una cuestión de estilo de programador: si se prefiere la composición de objetos o la herencia múltiple.

Nota: la clase establecida en la vista en el archivo NIB sigue siendo la misma. La subclase integrable solo se usa en el guión gráfico. La subclase no se puede usar para crear una instancia de la vista en código, por lo que no debería tener ninguna lógica adicional. Debe solamente contendrá el awakeAftergancho.

class MyCustomEmbeddableView: MyCustomView {
  override func awakeAfter(using aDecoder: NSCoder) -> Any? {
    return (UIView.instantiateViewFromNib("MyCustomView") as MyCustomView?)! as Any
  }
}

⚠️ El único inconveniente importante aquí es que si define restricciones de ancho, alto 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 desde el guión gráfico desde adentro hacia afuera, por lo que para cuando esas restricciones se hidratan en la supervista, el intercambio ya ha ocurrido. Las restricciones que solo involucran la vista en cuestión se instalan directamente en esa vista y, por lo tanto, se lanzan cuando se produce el intercambio a menos que se copien.

Tenga en cuenta que lo que está sucediendo aquí es que las restricciones instaladas en la vista en el guión gráfico se copian en la vista recién instanciada , que ya puede tener sus propias restricciones, 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
  }
}  

instantiateViewFromNibes una extensión de tipo seguro para UIView. Todo lo que hace es recorrer los objetos de la NIB hasta que encuentre 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
  }
}

Eso es alucinante. Sin embargo, si no me equivoco, esto solo funciona "en el guión gráfico". Si intenta crear una clase de este tipo de código en tiempo de ejecución, no creo que funcione. Yo creo.
Fattie

La subclase debería funcionar en código tan bien como la clase original para todos los intentos y propósitos. Si desea cargar la vista desde una punta en el código, simplemente la instanciará directamente usando la misma técnica. Todo lo que hace la subclase es tomar el código para crear una instancia de la vista desde una punta y ponerlo en un gancho para que el guión gráfico lo use.
Christopher Swasey

En realidad, yo estaba equivocado, que podría funcionar igual de bien si se puede crear una instancia de ella, pero no se puede, porque la vista de la SEMILLA tendrá la superclase como su tipo por lo que instantiateViewFromNibno se devuelve nada. No es un gran problema de ninguna manera IMO, la subclase es solo una invención para engancharse en el guión gráfico, todo el código debe estar en la clase original.
Christopher Swasey

1
¡Excelente! Una cosa me hizo tropezar porque tengo poca experiencia con xibs (solo trabajé con guiones gráficos y enfoques programáticos), dejando esto aquí en caso de que ayude a alguien: en el archivo .xib, debe seleccionar la vista de nivel superior y establecer su tipo de clase a MyCustomView. En mi xib faltaba la barra lateral interna izquierda por defecto; para encenderlo, hay un botón al lado del control de rasgos "Ver como: iPhone 7" cerca del lado inferior / izquierdo.
xaphod

55
Frena las restricciones cuando es reemplazado por otro objeto. :(
invoodoo

6

La mejor solución actualmente es usar un controlador de vista personalizado con su vista definida en un xib, y simplemente eliminar la propiedad de "vista" que Xcode crea dentro del guión gráfico al agregarle el controlador de vista (no olvide establecer el nombre de aunque la clase personalizada).

Esto hará que el tiempo de ejecución busque automáticamente el xib y lo cargue. Puede usar este truco para cualquier tipo de vista de contenedor o vista de contenido.


5

Pienso alternativeen usar XIB viewspara usar View Controller en un guión gráfico separado .

Luego, en guión gráfico principal en lugar de vista personalizada uso container viewcon Embed Seguey tienen StoryboardReferencea este controlador de vista personalizada que vista debe ser colocada dentro de otra vista en guión gráfico principal.

Luego podemos configurar la delegación y la comunicación entre este ViewController de inserción y el controlador de vista principal a través de la preparación para la segue . Este enfoque es diferente de mostrar UIView, pero se puede utilizar de manera mucho más simple y eficiente (desde la perspectiva de la programación) para lograr el mismo objetivo, es decir, tener una vista personalizada reutilizable que es visible en el guión gráfico principal

La ventaja adicional es que puede implementar su lógica en la clase CustomViewController y configurar toda la delegación y la preparación de la vista sin crear clases de controlador separadas (más difíciles de encontrar en el proyecto) y sin colocar el código repetitivo en el UIViewController principal usando Component. Creo que esto es bueno para componentes reutilizables ej. Componente del reproductor de música (similar a un widget) que se puede insertar en otras vistas.


De hecho, lamentablemente, es una solución mucho más simple que usar una vista personalizada xib :(
Wilson

1
después de ir en círculos con la cadena de creación de ciclo de vida xib de Apple en UIView personalizado, esta es la forma en que terminé yendo.
LightningStryk

En caso de que desee agregar la vista personalizada dinámicamente, aún tenemos que usar un controlador de vista separado (más preferiblemente XIB)
Satyam

5

Aunque las respuestas más populares funcionan bien, son conceptualmente incorrectas. Todos se usan File's ownercomo conexión entre las salidas de clase y los componentes de la interfaz de usuario. File's ownerse supone que se usa solo para objetos de nivel superior, no UIViews. Consulte el documento de desarrollador de Apple . Tener UIView como File's ownerconduce a estas consecuencias indeseables.

  1. Usted está obligado a usar contentViewdonde se supone que debe usarself . No solo es feo, sino también estructuralmente incorrecto porque la vista intermedia evita que la estructura de datos transmita su estructura de interfaz de usuario. Es como lo contrario de la IU declarativa.
  2. Solo puede tener una UIView por Xib. Se supone que un Xib tiene múltiples UIViews.

Hay una forma elegante de hacerlo sin usar File's owner. Por favor revise esta publicación de blog . Explica cómo hacerlo de la manera correcta.


no importa en absoluto, no explicaste por qué es malo. No puedo ver por qué. No hay efectos secundarios.
JBarros35

1

Aquí está la respuesta que siempre has querido. Puede crear su CustomViewclase, tener la instancia maestra de la misma en un xib con todas las subvistas y salidas. Luego, puede aplicar esa clase a cualquier instancia en sus guiones gráficos u otras xibs.

No es necesario jugar con el Propietario del archivo, o conectar salidas a un proxy o modificar el xib de una manera peculiar, o agregar una instancia de su vista personalizada como una subvista de sí mismo.

Solo haz esto:

  1. Importar marco BFWControls
  2. Cambia tu superclase de UIViewa NibView(o de UITableViewCella NibTableViewCell)

¡Eso es!

Incluso funciona con IBDesignable para referir su vista personalizada (incluidas las subvistas del xib) en tiempo de diseño en el guión gráfico.

Puede leer más sobre esto aquí: https://medium.com/build-an-app-like-lego/embed-a-xib-in-a-storyboard-953edf274155

Y puede obtener el marco de código abierto de BFWControls aquí: https://github.com/BareFeetWare/BFWControls

Y aquí hay un extracto simple del NibReplaceablecódigo que lo impulsa, en caso de que tenga curiosidad:
 https://gist.github.com/barefeettom/f48f6569100415e0ef1fd530ca39f5b4

Tom 👣


Solución ingeniosa, gracias!
avee

No quiero instalar un marco cuando debería proporcionarse de forma nativa.
JBarros35

0

Esta solución puede usarse incluso si su clase no tiene el mismo nombre que el XIB. Por ejemplo, si tiene una clase de controlador de vista base controllerA que tiene un nombre XIB controllerA.xib y lo subclasificó con controllerB y desea crear una instancia de controllerB en un guión gráfico, entonces puede:

  • crear el controlador de vista en el guión gráfico
  • establecer la clase del controlador al controladorB
  • eliminar la vista del controladorB en el guión gráfico
  • anular la vista de carga en el controlador A 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];
       }
}

0

Solución para Objective-C según los pasos descritos en la respuesta de Ben Patch .

Use la extensión para UIView:

@implementation UIView (NibLoadable)

- (UIView*)loadFromNib
{
    UIView *xibView = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class]) owner:self options:nil] firstObject];
    xibView.translatesAutoresizingMaskIntoConstraints = NO;
    [self addSubview:xibView];
    [xibView.topAnchor constraintEqualToAnchor:self.topAnchor].active = YES;
    [xibView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor].active = YES;
    [xibView.leftAnchor constraintEqualToAnchor:self.leftAnchor].active = YES;
    [xibView.rightAnchor constraintEqualToAnchor:self.rightAnchor].active = YES;
    return xibView;
}

@end

Crear archivos MyView.h, MyView.my MyView.xib.

Primero prepare su MyView.xibcomo la respuesta de Ben Patch dice, así que configure la clase MyViewpara el propietario del Archivo en lugar de la vista principal dentro de este XIB.

MyView.h:

#import <UIKit/UIKit.h>

IB_DESIGNABLE @interface MyView : UIView

@property (nonatomic, weak) IBOutlet UIView* someSubview;

@end

MyView.m:

#import "MyView.h"
#import "UIView+NibLoadable.h"

@implementation MyView

#pragma mark - Initializers

- (id)init
{
    self = [super init];
    if (self) {
        [self loadFromNib];
        [self internalInit];
    }
    return self;
}

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        [self loadFromNib];
        [self internalInit];
    }
    return self;
}

- (id)initWithCoder:(NSCoder *)aDecoder
{
    self = [super initWithCoder:aDecoder];
    if (self) {
        [self loadFromNib];
    }
    return self;
}

- (void)awakeFromNib
{
    [super awakeFromNib];
    [self internalInit];
}

- (void)internalInit
{
    // Custom initialization.
}

@end

Y luego solo crea tu vista programáticamente:

MyView* view = [[MyView alloc] init];

¡Advertencia! La vista previa de esta vista no se mostrará en Storyboard si usa WatchKit Extension debido a este error en Xcode> = 9.2: https://forums.developer.apple.com/thread/95616


Debería haber proporcionado la versión Swift. Muy pocos podrían estar usando ObjC todavía en este momento
Satyam
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.