Carregar visualização de um arquivo xib externo no storyboard

131

Eu quero usar uma exibição em vários controladores de exibição em um storyboard. Assim, pensei em projetar a visualização em um xib externo para que as mudanças sejam refletidas em todos os controladores de visualização. Mas como carregar uma visualização de um xib externo em um storyboard e isso é possível? Se não for esse o caso, que outras alternativas estão disponíveis para se adequar à situação acima?

Sebastian Hoffmann
fonte

Respostas:

132

Meu exemplo completo está aqui , mas fornecerei um resumo abaixo.

Layout

Adicione um arquivo .swift e .xib, cada um com o mesmo nome ao seu projeto. O arquivo .xib contém seu layout de exibição personalizado (usando restrições de layout automático, de preferência).

Torne o arquivo swift o proprietário do arquivo xib.

insira a descrição da imagem aqui Código

Adicione o seguinte código ao arquivo .swift e conecte as saídas e ações do arquivo .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
    }
}

Use-o

Use sua visualização personalizada em qualquer lugar do seu storyboard. Basta adicionar a UIViewe definir o nome da classe como seu nome personalizado.

insira a descrição da imagem aqui

Suragch
fonte
3
Não é que loadNibNamed chama init (coder :)? Estou com um problema ao tentar adaptar sua abordagem.
Fishman
@ Fishman, se você tentar carregar a exibição de forma programática (e não a partir do storyboard), ela travará porque não possui uma init(frame:). Veja este tutorial para mais detalhes.
Suragch
7
Outra causa comum de falha não é definir a exibição personalizada para o proprietário do arquivo . Veja o círculo vermelho na minha resposta.
Suragch
5
Sim, eu havia definido a classe da visualização raiz em vez do proprietário do arquivo e estava causando um loop infinito.
Devios1:
2
Depois de adicionar a linha view.autoresizingMask = [.flexibleWidth, .flexibleHeight] antes de self.addSubview (view), ele está funcionando perfeitamente.
Pramod Tapaniya
69

Por um tempo, a abordagem de Christopher Swasey foi a melhor abordagem que eu havia encontrado. Perguntei a alguns dos desenvolvedores seniores da minha equipe sobre isso e um deles tinha a solução perfeita ! Satisfaz todas as preocupações que Christopher Swasey abordou de forma tão eloquente e não exige código de subclasse padrão (minha principal preocupação com sua abordagem). Há uma pegadinha , mas, além disso, é bastante intuitiva e fácil de implementar.

  1. Crie uma classe UIView customizada em um arquivo .swift para controlar seu xib. ieMyCustomClass.swift
  2. Crie um arquivo .xib e estilize-o como desejar. ieMyCustomClass.xib
  3. Defina o File's Ownerarquivo .xib como sua classe personalizada ( MyCustomClass)
  4. GOTCHA: deixe o classvalor (sob identity Inspector) para sua visualização personalizada no arquivo .xib em branco. Portanto, sua visualização personalizada não terá classe especificada, mas terá o Proprietário de um arquivo especificado.
  5. Conecte suas tomadas como faria normalmente usando o Assistant Editor.
    • OBSERVAÇÃO: Se você observar Connections Inspector, notará que suas Referências não fazem referência à sua classe personalizada (ie MyCustomClass), mas sim à referência File's Owner. Como File's Owneré especificada como sua classe personalizada, as tomadas serão conectadas e funcionarão.
  6. Verifique se sua classe personalizada possui @IBDesignable antes da instrução de classe.
  7. Faça sua classe personalizada estar em conformidade com o NibLoadableprotocolo mencionado abaixo.
    • NOTA: Se o .swiftnome do arquivo de classe personalizado for diferente do .xibnome do arquivo, defina a nibNamepropriedade como o nome do .xibarquivo.
  8. Implemente required init?(coder aDecoder: NSCoder)e override init(frame: CGRect)ligue setupFromNib()como no exemplo abaixo.
  9. Adicione uma UIView ao storyboard desejado e defina a classe como o nome da sua classe personalizada (ou seja MyCustomClass).
  10. Assista ao IBDesignable em ação, pois ele desenha seu .xib no storyboard com toda a sua admiração e admiração.

Aqui está o protocolo que você deseja referenciar:

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

E aqui está um exemplo MyCustomClassdisso que implementa o protocolo (com o nome do arquivo .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()
    }

}

OBSERVAÇÃO: se você errar o Gotcha e definir o classvalor no arquivo .xib como sua classe personalizada, ele não será desenhado no storyboard e você receberá um EXC_BAD_ACCESSerro ao executar o aplicativo, pois ele fica preso em um loop infinito de tentando inicializar a classe da ponta usando o init?(coder aDecoder: NSCoder)método que chama Self.nib.instantiatee chama initnovamente.

Ben Patch
fonte
2
Aqui é outra ótima abordagem para isso, mas eu sinto a acima de um ainda melhor é: medium.com/zenchef-tech-and-product/...
Ben Patch de
4
A abordagem mencionada acima funciona perfeitamente e aplica a visualização ao vivo no storyboard. É absolutamente útil e incrível, grande!
Igor Leonovich
1
FYI: essa solução, usando a definição de restrição setupFromNib(), parece corrigir alguns problemas estranhos de layout automático com células de exibição de tabela de dimensionamento automático que contêm exibições criadas por XIB.
Gary
1
De longe a melhor solução! Eu amo @IBDesignablecompatibilidade. Não consigo acreditar por que o Xcode ou o UIKit não fornece algo como esse por padrão ao adicionar um arquivo UIView.
fl034
1
Não consigo fazer com que este funcione. Toda vez que eu configuro o Proprietário do arquivo no meu arquivo .xib, ele também define a classe personalizada.
Drewster
32

Supondo que você tenha criado um xib que deseja usar:

1) Crie uma subclasse personalizada do UIView (você pode ir para Arquivo -> Novo -> Arquivo ... -> Cocoa Touch Class. Verifique se "Subclasse de:" é "UIView").

2) Adicione uma visualização baseada no xib como uma subvisualização a esta visualização na inicialização.

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

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

Em 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) Onde você quiser usá-lo em seu storyboard, adicione uma UIView como faria normalmente, selecione a visualização recém-adicionada, vá para o Identity Inspector (o terceiro ícone no canto superior direito que parece um retângulo com linhas), e digite o nome da sua subclasse como "Classe" em "Classe personalizada".

user1021430
fonte
xibView.frame = self.frame;deve ser xibView.frame = CGRectMake(0, 0, self.frame.size.width, self.frame.size.height);, caso contrário, o xibView terá um deslocamento quando a visualização for adicionada ao storyboard.
BabyPanda
atrasado para a festa, mas parece que ele mudou para xibView.frame = self.bounds, que é um quadro sem compensação
Heavy_Bullets
20
Resultados em uma falha devido a recursão infinita. Carregar a ponta cria outra instância da subclasse.
David
2
A classe da visualização xib não deve ser a mesma desta nova subclasse. Se o xib for MyClass, você poderá criar essa nova classe MyClassContainer.
User1021430
a solução acima falhará com uma recursão infinita.
JBarros35 29/06
27

Eu sempre achei a solução "adicione-a como uma subvisão" insatisfatória, pois ela se aperta com (1) autolayout, (2) @IBInspectablee (3) tomadas. Em vez disso, deixe-me apresentar-lhe a magia de awakeAfter:um NSObjectmétodo.

awakeAfterpermite trocar o objeto realmente acordado de um NIB / Storyboard por um objeto completamente diferente. Esse objeto é então submetido ao processo de hidratação, awakeFromNibchamado, adicionado como uma vista, etc.

Podemos usar isso em uma subclasse "recorte de papelão" de nossa visão, cujo único objetivo será carregar a visão do NIB e devolvê-la para uso no Storyboard. A subclasse incorporável é especificada no inspetor de identidade da exibição Storyboard, em vez da classe original. Na verdade, ele não precisa ser uma subclasse para que isso funcione, mas torná-la uma subclasse é o que permite ao IB ver quaisquer propriedades do IBInspectable / IBOutlet.

Esse padrão adicional pode parecer abaixo do ideal - e, em certo sentido, é, porque o ideal UIStoryboardseria lidar com isso sem problemas -, mas tem a vantagem de deixar o NIB original e a UIViewsubclasse completamente inalterada. O papel que desempenha é basicamente o de uma classe de adaptador ou ponte e é perfeitamente válido, em termos de design, como uma classe adicional, mesmo que seja lamentável. Por outro lado, se você preferir ser parcimonioso com suas classes, a solução do @ BenPatch funciona implementando um protocolo com algumas outras pequenas alterações. A questão de qual solução é melhor se resume a uma questão de estilo de programador: se prefere a composição do objeto ou a herança múltipla.

Nota: a classe configurada na visualização no arquivo NIB permanece a mesma. A subclasse incorporável é usada apenas no storyboard. A subclasse não pode ser usada para instanciar a visualização no código; portanto, ela não deve ter nenhuma lógica adicional. Deve conter apenas o awakeAftergancho.

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

⚠️ A única desvantagem aqui é que, se você definir restrições de largura, altura ou proporção no storyboard que não se relacionam a outra vista, elas deverão ser copiadas manualmente. As restrições que relacionam duas visualizações são instaladas no ancestral comum mais próximo e as visualizações são despertadas do storyboard de dentro para fora; portanto, quando essas restrições são hidratadas na superview, a troca já ocorreu. As restrições que envolvem apenas a visualização em questão são instaladas diretamente nessa visualização e, portanto, são lançadas quando a troca ocorre, a menos que sejam copiadas.

Observe que o que está acontecendo aqui é que as restrições instaladas na exibição no storyboard são copiadas para a exibição recém-instanciada , que já pode ter restrições próprias, definidas em seu arquivo de ponta. Aqueles não são afetados.

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é uma extensão de tipo seguro para UIView. Tudo o que faz é percorrer os objetos do NIB até encontrar um que corresponda ao tipo. Observe que o tipo genérico é o valor de retorno , portanto, o tipo deve ser especificado no site de chamada.

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
  }
}
Christopher Swasey
fonte
Isso é incompreensível. Se não me engano, isso funciona apenas "no storyboard" - se você tentar criar essa classe no código em tempo de execução, não acho que funcione. Acredito.
Fattie
A subclasse deve funcionar no código da mesma forma que na classe original para todos os efeitos. Se você deseja carregar a visualização de uma ponta no código, instancie-a diretamente usando a mesma técnica. Tudo o que a subclasse faz é pegar o código para instanciar a exibição de uma ponta e colocá-lo em um gancho para o storyboard usar.
Christopher Swasey
Na verdade, eu estava errado, ele iria funcionar tão bem se você pudesse instanciá-lo, mas você não pode, porque a vista no NIB terá a superclasse como seu tipo assim instantiateViewFromNibnão vai retornar nada. Não é grande coisa de qualquer maneira, como a IMO, a subclasse é apenas um artifício para se conectar ao storyboard, todo o código deve estar na classe original.
Christopher Swasey
1
Ótimo! Uma coisa me fez tropeçar porque tenho pouca experiência com xibs (eu só trabalhei com storyboards e abordagens programáticas), deixando isso aqui para o caso de ajudar alguém: no arquivo .xib, você precisa selecionar a visualização de nível superior e definir seu tipo de classe para MyCustomView. No meu xib, a barra lateral esquerda estava ausente por padrão; para ativá-lo, há um botão ao lado do controle "Visualizar como: iPhone 7" próximo ao lado inferior / esquerdo.
xaphod
5
Ele freia restrições quando é substituído por outro objeto. :(
invoodoo
6

Atualmente, a melhor solução é usar apenas um controlador de visualização personalizado com sua visualização definida em um xib e simplesmente excluir a propriedade "view" que o Xcode cria dentro do storyboard ao adicionar o controlador de visualização (não se esqueça de definir o nome de a classe personalizada).

Isso fará com que o tempo de execução procure automaticamente o xib e o carregue. Você pode usar esse truque para qualquer tipo de exibição de contêiner ou exibição de conteúdo.

Ben G
fonte
5

Penso alternativeem usar XIB viewspara usar View Controller em storyboard separado .

Então, em storyboard principal no lugar de uso exibição personalizada container viewcom Embed Seguee ter StoryboardReferencea este controlador de exibição personalizada que vista deve ser colocado dentro de outro ponto de vista em storyboard principal.

Em seguida, podemos configurar a delegação e a comunicação entre esse ViewController incorporado e o controlador de exibição principal por meio de preparar para segue . Essa abordagem é diferente da exibição do UIView, mas muito mais simples e mais eficiente (da perspectiva da programação) pode ser utilizada para atingir o mesmo objetivo, ou seja, ter uma visualização personalizada reutilizável que é visível no storyboard principal

A vantagem adicional é que você pode implementar sua lógica na classe CustomViewController e configurar toda a delegação e exibir a preparação sem criar classes de controlador separadas (mais difíceis de encontrar no projeto) e sem colocar o código padrão no UIViewController principal usando Component. Eu acho que isso é bom para componentes reutilizáveis ​​ex. Componente Music Player (como widget) que pode ser incorporado em outras visualizações.

Michał Ziobro
fonte
Na verdade, infelizmente, muito mais simples solução do que usar costume vista xib :(
Wilson
1
depois de seguir em círculos com a cadeia de criação do ciclo de vida xib da Apple no UIView personalizado, foi assim que acabei indo.
LightningStryk
Caso você queira adicionar a visualização personalizada dinamicamente, ainda precisamos usar um controlador de visualização separado (de preferência XIB)
Satyam
5

Embora as respostas mais populares funcionem bem, elas estão conceitualmente erradas. Todos eles usam File's ownercomo conexão entre as saídas da classe e os componentes da interface do usuário. File's ownerdeve ser usado apenas para objetos de nível superior e não UIViews. Confira o documento de desenvolvedor da Apple . Ter o UIView como resultado File's ownerdessas consequências indesejáveis.

  1. Você é forçado a usar contentViewonde deve usar self. Não é apenas feio, mas também estruturalmente errado, porque a visualização intermediária impede a estrutura de dados de transmitir sua estrutura de interface do usuário. É como o oposto da interface do usuário declarativa.
  2. Você pode ter apenas um UIView por Xib. Um Xib deve ter vários UIViews.

Há uma maneira elegante de fazer isso sem usar File's owner. Por favor, verifique esta postagem do blog . Explica como fazê-lo da maneira certa.

Ingun 전인 건
fonte
não importa nada, você não explicou por que isso é ruim. Não vejo o porquê. Não tem efeitos colaterais.
JBarros35 29/06
1

Aqui está a resposta que você sempre quis. Você pode apenas criar sua CustomViewclasse, ter a instância principal dela em um xib com todas as subvisões e saídas. Em seguida, você pode aplicar essa classe a quaisquer instâncias em seus storyboards ou outros xibs.

Não há necessidade de mexer com o proprietário do arquivo, ou conectar tomadas a um proxy ou modificar o xib de uma maneira peculiar ou adicionar uma instância de sua visualização personalizada como uma sub-visualização própria.

Apenas faça o seguinte:

  1. Importar estrutura BFWControls
  2. Alterar sua superclasse de UIViewpara NibView(ou de UITableViewCellpara NibTableViewCell)

É isso aí!

Até funciona com o IBDesignable para referenciar sua visualização personalizada (incluindo as subvisões do xib) em tempo de design no storyboard.

Você pode ler mais sobre isso aqui: https://medium.com/build-an-app-like-lego/embed-a-xib-in-a-storyboard-953edf274155

E você pode obter a estrutura BFWControls de código aberto aqui: https://github.com/BareFeetWare/BFWControls

E aqui está uma simples extração do NibReplaceablecódigo que o move, caso você esteja curioso:
 https://gist.github.com/barefeettom/f48f6569100415e0ef1fd530ca39f5b4

Tom 👣

barefeettom
fonte
Solução bacana, obrigado!
avee 26/06
Eu não quero instalar uma estrutura quando ela deve ser fornecida nativamente.
JBarros35 29/06
0

Esta solução pode ser usada mesmo que sua classe não tenha o mesmo nome que o XIB. Por exemplo, se você tiver uma classe de controlador de exibição de base controllerA que possui um nome XIB controllerA.xib e a subclassificou com controllerB e deseja criar uma instância de controllerB em um storyboard, poderá:

  • crie o controlador de exibição no storyboard
  • defina a classe do controlador como controllerB
  • excluir a visualização do controllerB no storyboard
  • substituir a visualização de carga no 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];
       }
}
otusweb
fonte
0

Solução para Objective-C de acordo com as etapas descritas na resposta de Ben Patch .

Use a extensão 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

Crie arquivos MyView.h, MyView.me MyView.xib.

Primeiro prepare sua respostaMyView.xib como Ben Patch diz, então defina a classe MyViewpara o proprietário do arquivo em vez da visualização principal dentro deste 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

E mais tarde, basta criar sua exibição programaticamente:

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

Aviso! A visualização dessa exibição não será mostrada no Storyboard se você usar a Extensão WatchKit devido a esse bug no Xcode> = 9.2: https://forums.developer.apple.com/thread/95616

Ariel Bogdziewicz
fonte
Deveria ter fornecido a versão Swift. Muito poucos podem estar usando ObjC ainda neste momento
Satyam