Como faço para criar uma classe de visualização iOS personalizada e instanciar várias cópias dela (em IB)?

114

No momento, estou criando um aplicativo que terá vários temporizadores, que são basicamente iguais.

Eu quero criar uma classe personalizada que usa todo o código para os temporizadores, bem como o layout / animações, para que eu possa ter 5 temporizadores idênticos que operam independentemente um do outro.

Criei o layout usando IB (xcode 4.2) e todo o código para os temporizadores está atualmente apenas na classe viewcontroller.

Estou tendo dificuldade para entender como encapsular tudo em uma classe personalizada e, em seguida, adicioná-la ao viewcontroller, qualquer ajuda seria muito apreciada.

Michael Campsall
fonte
Para qualquer um que pesquise aqui ... já se passaram cinco anos desde que as visualizações de contêiner chegaram ao iOS! As visualizações de contêiner são a ideia básica e central de como você faz tudo no iOS agora. Eles são incrivelmente simples de usar e há vários tutoriais excelentes (com cinco anos!) Sobre visualizações de contêineres, por exemplo , exemplo
Fattie
2
@JoeBlow Exceto que você não pode usar uma visualização de contêiner em um UITableViewCellou UICollectionViewCell. No meu caso, preciso de uma visão pequena, mas bastante complexa, que possa usar muitas vezes em controladores e visualizações de coleção. E os designers continuam retrabalhando, então eu quero um único lugar onde o layout seja definido. Portanto, um bico.
Echelon

Respostas:

142

Bem, para responder conceitualmente, seu cronômetro provavelmente deve ser uma subclasse de em UIViewvez de NSObject.

Para instanciar uma instância do seu temporizador no IB, simplesmente arraste e UIViewsolte-o na visualização do seu controlador de visualização e defina sua classe com o nome da classe do seu temporizador.

insira a descrição da imagem aqui

Lembre-se de #importsua classe de cronômetro em seu controlador de visualização.

Editar: para design IB (para instanciação de código, consulte o histórico de revisão)

Não estou muito familiarizado com o storyboard, mas sei que você pode construir sua interface em IB usando um .xibarquivo que é quase idêntico ao da versão do storyboard; Você deve até mesmo ser capaz de copiar e colar suas visualizações como um todo da interface existente para o .xibarquivo.

Para testar isso, criei um novo vazio .xibchamado "MyCustomTimerView.xib". Em seguida, adicionei uma visualização e a ela adicionei um rótulo e dois botões. Igual a:

insira a descrição da imagem aqui

Criei uma nova subclasse de classe Objectiva C UIViewchamada "MyCustomTimer". No meu, .xibeu defini minha classe Proprietário do arquivo como MyCustomTimer . Agora estou livre para conectar ações e tomadas como qualquer outra visualização / controlador. O .harquivo resultante tem a seguinte aparência:

@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

O único obstáculo que falta superar é colocar isso .xibna minha UIViewsubclasse. Usar um .xibreduz drasticamente a configuração necessária. E como você está usando storyboards para carregar os cronômetros, sabemos que -(id)initWithCoder:é o único inicializador que será chamado. Então, aqui está a aparência do arquivo de implementação:

#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

O método nomeado loadNibNamed:owner:options:faz exatamente o que parece. Ele carrega o Nib e define a propriedade "Proprietário do arquivo" como self. Extraímos o primeiro objeto do array e essa é a visão raiz do Nib. Adicionamos a visualização como uma subvisualização e Voila está na tela.

Obviamente, isso apenas muda a cor de fundo do rótulo quando os botões são pressionados, mas este exemplo deve ajudá-lo no bom caminho.

Notas baseadas em comentários:

É importante notar que, se você está tendo problemas de recursão infinita, provavelmente não percebeu o truque sutil dessa solução. Não está fazendo o que você pensa que está fazendo. A visão que é colocada no storyboard não é vista, mas em vez disso carrega outra visão como uma subvisualização. A visualização que ele carrega é a visualização definida na ponta. O "proprietário do arquivo" na ponta é aquela visão invisível. A parte legal é que essa visão invisível ainda é uma classe Objective-C que pode ser usada como uma espécie de controlador de visão para a visão que ela traz da ponta. Por exemplo, os IBActionmétodos na MyCustomTimerclasse são algo que você esperaria mais em um controlador de visualização do que em uma visualização.

Como uma observação lateral, alguns podem argumentar que isso quebra MVCe eu concordo um pouco. Do meu ponto de vista, está mais relacionado a um costume UITableViewCell, que às vezes também precisa ser parte do controlador.

É importante notar também que essa resposta foi para fornecer uma solução muito específica; crie uma ponta que pode ser instanciada várias vezes na mesma visualização em um storyboard. Por exemplo, você pode facilmente imaginar seis desses temporizadores, todos em uma tela de iPad ao mesmo tempo. Se você só precisa especificar uma visualização para um controlador de visualização que deve ser usado várias vezes em seu aplicativo, a solução fornecida por jyavenard para esta questão é quase certamente uma solução melhor para você.

NJones
fonte
Na verdade, esta parece ser uma questão separada. Mas acho que pode ser respondido rapidamente, então aqui vai. Cada vez que você cria uma nova notificação, é uma nova notificação, mas se você precisar que eles tenham o mesmo nome, deseja adicionar algo para diferenciá-los, então use a userInfopropriedade na notificação@property(nonatomic,copy) NSDictionary *userInfo;
NJones
28
Recebo uma recursão infinita de initWithCoder. Alguma ideia?
desaparecido em
6
Desculpe por reviver um tópico antigo, certificar- File's Ownerse de que está definido com o nome da classe resolveu meu problema de recursão infinita.
themanatuf
20
Outro motivo para recursão infinita é definir a visualização raiz no xib para sua classe personalizada. Se você não quiser fazer isso, deixe como "UIView" padrão
XY
11
Há apenas uma coisa que me chama a atenção sobre esta abordagem ... Não incomoda ninguém que 2 Views estejam na mesma classe agora? o arquivo original .h / .m herdando de UIView sendo o primeiro e [self addSubview:[[[NSBundle mainBundle] loadNibNamed:@"MyCustomTimerView" owner:self options:nil] objectAtIndex:0]]; adicionando o segundo UIView ... Isso parece meio estranho para mim, ninguém mais se sente da mesma maneira? Se sim, isso é algo feito pela maçã por design? Ou alguém encontrou a solução ao fazer isso?
SH
137

Exemplo rápido

Atualizado para Xcode 10 e Swift 4

Aqui está um guia básico. Eu originalmente aprendi muito sobre isso assistindo a esta série de vídeos no Youtube . Posteriormente, atualizei minha resposta com base neste artigo .

Adicionar arquivos de visualização personalizada

Os dois arquivos a seguir formarão sua visualização personalizada:

  • arquivo .xib para conter o layout
  • arquivo .swift como UIViewsubclasse

Os detalhes para adicioná-los estão abaixo.

Arquivo Xib

Adicione um arquivo .xib ao seu projeto (Arquivo> Novo> Arquivo ...> Interface do usuário> Exibir) . Estou chamando o meu ReusableCustomView.xib.

Crie o layout que você deseja que a exibição personalizada tenha. Como exemplo, farei um layout com a UILabele a UIButton. É uma boa ideia usar o layout automático para que as coisas sejam redimensionadas automaticamente, não importando o tamanho definido posteriormente. (Usei Freeform para o tamanho xib no inspetor de atributos para poder ajustar as métricas simuladas, mas isso não é necessário.)

insira a descrição da imagem aqui

Arquivo Swift

Adicione um arquivo .swift ao seu projeto (Arquivo> Novo> Arquivo ...> Código-fonte> Arquivo Swift) . É uma subclasse de UIViewe estou chamando de minha ReusableCustomView.swift.

import UIKit
class ResuableCustomView: UIView {

}

Faça do arquivo Swift o proprietário

Volte para o arquivo .xib e clique em "Proprietário do arquivo" no esboço do documento. No Inspetor de identidade, escreva o nome do seu arquivo .swift como o nome da classe personalizada.

insira a descrição da imagem aqui

Adicionar código de visualização personalizada

Substitua o ReusableCustomView.swiftconteúdo do arquivo pelo seguinte 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
    }
}

Certifique-se de usar a grafia correta para o nome do arquivo .xib.

Conecte os Outlets e Ações

Conecte os pontos de venda e ações ao arrastar o controle do rótulo e botão no layout xib para o código de visualização personalizada rápida.

Use sua visualização personalizada

Sua visualização personalizada está concluída agora. Tudo o que você precisa fazer é adicionar um UIViewonde quiser em seu storyboard principal. Defina o nome da classe da visualização ReusableCustomViewno Identity Inspector.

insira a descrição da imagem aqui

Suragch
fonte
31
Importante não definir a classe de exibição Xib como ResuableCustomView, mas como Proprietário deste Xib. Caso contrário, você terá erro.
RealNmae
5
Sim, bom lembrete. Não sei quanto tempo perdi tentando resolver problemas de recursão infinita causados ​​pela definição da visualização do Xib (em vez do Proprietário do arquivo) para o nome da classe.
Suragch
2
@TomCalmon, refiz esse projeto no Xcode 8 com Swift 3 e o Auto Layout estava funcionando bem para mim.
Suragch
6
Se alguém mais tiver problemas com a renderização da visualização no IB, descobri que mudar Bundle.mainpara Bundle(for: ...)resolve. Aparentemente, Bundle.mainnão está disponível em tempo de design.
crizzis
4
O motivo pelo qual isso não funciona com @IBDesignable é que você não o implementou init(frame:). Depois de adicionar isso (e colocar todo o código de inicialização em alguma commonInit()função), ele funciona bem e é exibido em IB. Veja mais informações aqui: stackoverflow.com/questions/31265906/…
Dimitris
39

Resposta para controladores de visualização, não visualizações :

Existe uma maneira mais fácil de carregar um xib de um storyboard. Digamos que seu controlador seja do tipo MyClassController do qual herda UIViewController.

Você adiciona um UIViewControllerusando IB em seu storyboard; mude o tipo de classe para MyClassController. Exclua a visualização que foi adicionada automaticamente ao storyboard.

Certifique-se de que o XIB que você deseja chamar se chama MyClassController.xib.

Quando a classe for instanciada durante o carregamento do storyboard, o xib será carregado automaticamente. A razão para isso é devido à implementação padrão UIViewControllerque chama o XIB nomeado com o nome da classe.

jyavenard
fonte
1
Eu aumento as visualizações personalizadas o tempo todo, mas nunca conheci esse pequeno truque para carregá-lo no storyboard. Muito apreciado!
MrTristan
38
A questão é sobre visualizações, não sobre controladores de visualização.
Ricardo
Adicionado título para esclarecimento, "Resposta para controladores de visualização, não visualizações:"
nacho4d
1
Não parece estar funcionando no iOS 9.1. Se eu remover a visualização criada automaticamente (padrão), ela travará após retornar do viewDidLoad do VC. Não acho que a visualização no XIB seja conectada no lugar da visualização padrão.
Jeff
1
A exceção que estou recebendo é 'Could not load NIB in bundle: 'NSBundle </Users/me/.../MyAppName.app> (loaded)' with name 'uB0-aR-WjG-view-4OA-9v-tyV''. Observe o whacko nibName. Estou usando o nome de classe correto para o na ponta.
Jeff
16

Esta não é realmente uma resposta, mas acho que é útil compartilhar essa abordagem.

Objective-C

  1. Importe CustomViewWithXib.h e CustomViewWithXib.m para seu projeto
  2. Crie os arquivos de visualização customizados com o mesmo nome (.h / .m / .xib)
  3. Herdar sua classe personalizada de CustomViewWithXib

Rápido

  1. Importe CustomViewWithXib.swift para o seu projeto
  2. Crie os arquivos de visualização personalizados com o mesmo nome (.swift e .xib)
  3. Herdar sua classe personalizada de CustomViewWithXib

Opcional:

  1. Vá para seu arquivo xib, defina o proprietário com seu nome de classe personalizado se precisar conectar alguns elementos (para obter mais detalhes, consulte a parte Tornar o arquivo Swift o proprietário das respostas @Suragch)

É tudo, agora você pode adicionar sua visualização personalizada ao storyboard e ela será exibida :)

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

Você pode encontrar alguns exemplos aqui .

Espero que ajude.

HamzaGhazouani
fonte
então, como podemos adicionar iboutlet para os uicontrols de visualização
Amr Angry
1
Ei @AmrAngry, para conectar as visualizações você deve fazer o 4 passo: Vá para o seu arquivo xib, em "Proprietário do arquivo" defina sua classe, e arraste e solte suas visualizações na interface tocando no toque "ctrl", eu atualizo o fonte de código com este exemplo, não hesite se você tiver alguma dúvida, espero que ajude
HamzaGhazouani
muito obrigado pelo seu replay, eu já faço isso, mas acho que meu problema não é nesta parte. Meu problema é adicionar um UIDatePIcker e tentar adicionar uma ação de destino para este controle do ViewController para definir e o valor do evento uiControleer é alterado, mas o método nunca é chamado / fire
Amr Angry
1
@AmrAngry Eu atualizo o exemplo adicionando a visualização do seletor, pode ser que ajude você :) github.com/HamzaGhazouani/Stackoverflow/tree/master/…
HamzaGhazouani
1
Obrigado. Eu estava procurando por isso há muito tempo.
MH175