Em um storyboard, como faço para criar uma célula personalizada para uso com vários controladores?

216

Estou tentando usar storyboards em um aplicativo em que estou trabalhando. No aplicativo, existem listas e usuários e cada um contém uma coleção do outro (membros de uma lista, listas pertencentes a um usuário). Então, consequentemente, eu tenho ListCelle UserCellaulas. O objetivo é que eles sejam reutilizáveis ​​em todo o aplicativo (ou seja, em qualquer um dos meus controladores de tableview).

É aí que estou tendo um problema.

Como crio uma célula de tableview personalizada no storyboard que pode ser reutilizada em qualquer controlador de exibição?

Aqui estão as coisas específicas que eu tentei até agora.

  • No Controlador nº 1, adicionei uma célula protótipo, defina a classe como minha UITableViewCellsubclasse, defina o ID de reutilização, adicione os rótulos e conecte-os às tomadas da classe. No Controlador nº 2, adicionou uma célula protótipo vazia, defina-a na mesma classe e reutilize o ID como antes. Quando é executado, os rótulos nunca aparecem quando as células são mostradas no Controlador nº 2. Funciona bem no Controlador nº 1.

  • Projetou cada tipo de célula em um NIB diferente e conectado à classe de célula apropriada. No storyboard, adicionei uma célula protótipo vazia e defina sua classe e ID de reutilização para se referir à minha classe de célula. Nos viewDidLoadmétodos dos controladores , registrei esses arquivos NIB para o ID de reutilização. Quando mostradas, as células nos dois controladores estavam vazias como o protótipo.

  • Os protótipos mantidos nos dois controladores esvaziam e definem a classe e reutilizam o ID para a minha classe de célula. Construiu a interface do usuário das células inteiramente em código. As células funcionam perfeitamente em todos os controladores.

No segundo caso, suspeito que o protótipo esteja sempre substituindo o NIB e, se eu matar as células do protótipo, o registro do meu NIB para o ID de reutilização funcionará. Mas então eu não seria capaz de configurar seques das células para outros quadros, o que é realmente o objetivo de usar storyboards.

No final do dia, quero duas coisas: conecte fluxos baseados em tableview no storyboard e defina layouts de célula visualmente, e não no código. Não vejo como chegar até os dois até agora.

Penhasco W
fonte

Respostas:

205

Pelo que entendi, você deseja:

  1. Crie uma célula no IB que possa ser usada em várias cenas do storyboard.
  2. Configure segues de storyboard exclusivos a partir dessa célula, dependendo da cena em que a célula está.

Infelizmente, atualmente não há como fazer isso. Para entender por que suas tentativas anteriores não funcionaram, você precisa entender mais sobre como as células de exibição de tabela de protótipos e storyboards funcionam. (Se você não se importa com o motivo pelo qual essas outras tentativas não funcionaram, fique à vontade para sair agora. Não tenho nenhuma solução mágica para você, além de sugerir que você registre um bug.)

Um storyboard é, em essência, não muito mais do que uma coleção de arquivos .xib. Quando você carrega um controlador de exibição de tabela que possui algumas células protótipo de um storyboard, eis o que acontece:

  • Cada célula protótipo é na verdade sua própria mini ponta embutida. Portanto, quando o controlador de exibição de tabela está sendo carregado, ele percorre cada uma das pontas e chamadas da célula do protótipo -[UITableView registerNib:forCellReuseIdentifier:].
  • A visualização da tabela solicita ao controlador as células.
  • Você provavelmente liga -[UITableView dequeueReusableCellWithIdentifier:]
  • Quando você solicita uma célula com um determinado identificador de reutilização, ela verifica se possui uma ponta registrada. Se isso acontecer, ele instancia uma instância dessa célula. Isso é composto pelas seguintes etapas:

    1. Veja a classe da célula, conforme definido na ponta da célula. Ligue [[CellClass alloc] initWithCoder:].
    2. O -initWithCoder:método passa e adiciona subvisões e define propriedades que foram definidas na ponta. ( IBOutletprovavelmente também estão ligados aqui, embora eu não tenha testado; pode acontecer em -awakeFromNib)
  • Você configura seu celular da maneira que desejar.

O importante a ser observado aqui é que há uma distinção entre a classe da célula e a aparência visual da célula. Você pode criar duas células de protótipo separadas da mesma classe, mas com suas subvisões dispostas de maneira completamente diferente. De fato, se você usar os UITableViewCellestilos padrão , é exatamente isso que está acontecendo. O estilo "Padrão" e o estilo "Legenda", por exemplo, são ambos representados pela mesma UITableViewCellclasse.

Isso é importante : A classe da célula não tem uma correlação individual com uma hierarquia de exibição específica . A hierarquia de visualizações é determinada inteiramente pelo conteúdo da célula protótipo que foi registrada neste controlador em particular.

Observe também que o identificador de reutilização da célula não foi registrado em algum dispensário global de células. O identificador de reutilização é usado apenas no contexto de uma única UITableViewinstância.


Dada essa informação, vejamos o que aconteceu nas suas tentativas acima.

No Controlador nº 1, adicionei uma célula protótipo, defina a classe como minha subclasse UITableViewCell, defina o ID de reutilização, adicione os rótulos e os conecte às saídas da classe. No Controlador nº 2, adicionou uma célula protótipo vazia, defina-a na mesma classe e reutilize o ID como antes. Quando é executado, os rótulos nunca aparecem quando as células são mostradas no Controlador nº 2. Funciona bem no Controlador nº 1.

Isso é esperado. Embora as duas células tenham a mesma classe, a hierarquia de exibição que foi passada para a célula no Controlador nº 2 foi totalmente desprovida de subvisões. Então você tem uma célula vazia, que é exatamente o que você coloca no protótipo.

Projetou cada tipo de célula em um NIB diferente e conectado à classe de célula apropriada. No storyboard, adicionei uma célula protótipo vazia e defina sua classe e ID de reutilização para se referir à minha classe de célula. Nos métodos viewDidLoad dos controladores, registrou esses arquivos NIB para o ID de reutilização. Quando mostradas, as células nos dois controladores estavam vazias como o protótipo.

Novamente, isso é esperado. O identificador de reutilização não é compartilhado entre cenas ou pontas do storyboard, portanto, o fato de todas essas células distintas terem o mesmo identificador de reutilização não faz sentido. A célula que você voltar da tableview terá uma aparência que corresponde à célula protótipo na cena do storyboard.

Essa solução estava próxima, no entanto. Como você observou, você poderia simplesmente chamar de forma programática -[UITableView registerNib:forCellReuseIdentifier:], passando o que UINibcontinha a célula, e recuperaria a mesma célula. (Isso não ocorre porque o protótipo estava "substituindo" a ponta; você simplesmente não havia registrado a ponta na tableview, por isso ainda estava olhando a ponta incorporada no storyboard.) Infelizmente, há uma falha nessa abordagem - não há como conectar segues de storyboard a uma célula em uma ponta independente.

Os protótipos mantidos nos dois controladores esvaziam e definem a classe e reutilizam o ID para a minha classe de célula. Construiu a interface do usuário das células inteiramente em código. As células funcionam perfeitamente em todos os controladores.

Naturalmente. Felizmente, isso não é surpreendente.


Então, é por isso que não funcionou. Você pode projetar suas células em pontas independentes e usá-las em várias cenas de storyboard; no momento, você não pode conectar seques de storyboard a essas células. Felizmente, você aprendeu alguma coisa no processo de ler isso.

BJ Homer
fonte
Ah, entendi. Você entendeu meu mal-entendido - a hierarquia de visualizações é completamente independente da minha classe. Óbvio em retrospecto! Obrigado pela ótima resposta.
Cliff W
Não é mais impossível, ao que parece: stackoverflow.com/questions/8574188/…
Rich Apodaca 18/12
7
@RichApodaca Mencionei essa solução na minha resposta. Mas não está no storyboard; está em uma ponta separada. Portanto, você não pode conectar seguidores ou fazer outras coisas de storyboard. Portanto, não trata totalmente da questão inicial.
BJ Homer
No XCode8, a seguinte solução alternativa parece funcionar se você deseja uma solução apenas para storyboard. Etapa 1) Crie sua célula protótipo em uma tableview no ViewController # 1 e associe-a à classe UITableViewCell personalizada Etapa 2) Copie / cole essa célula na tableview do ViewController # 2. Com o tempo, lembre-se de propagar manualmente as atualizações das cópias da célula, excluindo as cópias feitas no storyboard e colando novamente no protótipo atualizado.
jengelsma
Ótima resposta, eu tenho uma pergunta de acompanhamento:> "Um storyboard é, em essência, não muito mais do que uma coleção de arquivos .xib", se este for o caso, por que é tão difícil incorporar um xib no um storyboard?
Willcwf
58

Apesar da ótima resposta de BJ Homer, sinto que tenho uma solução. No que diz respeito aos meus testes, ele funciona.

Conceito: Crie uma classe customizada para a célula xib. Lá, você pode esperar por um evento de toque e executar as etapas programaticamente. Agora, tudo o que precisamos é uma referência ao controlador que está executando o Segue. Minha solução é configurá-lo tableView:cellForRowAtIndexPath:.

Exemplo

Eu tenho uma DetailedTaskCell.xibcélula de tabela que eu gostaria de usar em várias visualizações de tabela:

DetalhadoTaskCell.xib

Há uma classe personalizada TaskGuessTableCellpara essa célula:

insira a descrição da imagem aqui

É aqui que a mágica acontece.

// TaskGuessTableCell.h
#import <Foundation/Foundation.h>

@interface TaskGuessTableCell : UITableViewCell
@property (nonatomic, weak) UIViewController *controller;
@end

// TashGuessTableCell.m
#import "TaskGuessTableCell.h"

@implementation TaskGuessTableCell

@synthesize controller;

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    NSIndexPath *path = [controller.tableView indexPathForCell:self];
    [controller.tableView selectRowAtIndexPath:path animated:NO scrollPosition:UITableViewScrollPositionNone];
    [controller performSegueWithIdentifier:@"FinishedTask" sender:controller];
    [super touchesEnded:touches withEvent:event];
}

@end

Eu tenho vários Segues mas todos eles têm o mesmo nome: "FinishedTask". Se você precisar ser flexível aqui, sugiro adicionar outra propriedade.

O ViewController fica assim:

// LogbookViewController.m
#import "LogbookViewController.h"
#import "TaskGuessTableCell.h"

@implementation LogbookViewController

- (void)viewDidLoad
{
    [super viewDidLoad]

    // register custom nib
    [self.tableView registerNib:[UINib nibWithNibName:@"DetailedTaskCell" bundle:[NSBundle mainBundle]] forCellReuseIdentifier:@"DetailedTaskCell"];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    TaskGuessTableCell *cell;

    cell = [tableView dequeueReusableCellWithIdentifier:@"DetailedTaskCell"];
    cell.controller = self; // <-- the line that matters
    // if you added the seque property to the cell class, set that one here
    // cell.segue = @"TheSegueYouNeedToTrigger";
    cell.taskTitle.text  = [entry title];
    // set other outlet values etc. ...

    return cell;
}

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    if([[segue identifier] isEqualToString:@"FinishedTask"])
    {
        // do what you have to do, as usual
    }

}

@end

Pode haver maneiras mais elegantes de conseguir o mesmo, mas - funciona! :)

ericteubert
fonte
1
Obrigado, estou implementando essa abordagem no meu projeto. Você pode substituir esse método para não precisar obter o indexPath e selecionar a linha: - (void) setSelected: (BOOL) selecionado animado: (BOOL) animado {[super setSelected: selecionado animado: animado]; se (selecionado) [autocontrolador performSegueWithIdentifier: self.segue sender: self]; } Eu pensei que super selecionaria a célula ao chamar [super touchesEnded: toca comEvent: event] ;. Você sabe quando é selecionado se não estiver lá?
thejaz
9
Observe que, com esta solução, você está acionando o segue toda vez que um toque termina dentro de uma célula. Isso inclui se você está simplesmente rolando a célula, e não tentando selecioná-la. Você pode ter melhor sorte substituindo -setSelected:a célula e acionando o segue somente ao fazer a transição de NOpara YES.
BJ Homer
Eu tenho mais sorte com setSelected:, BJ. Obrigado. Na verdade, é uma solução deselegante ( parece errada), mas, ao mesmo tempo, funciona, então estou usando até que isso seja corrigido (ou que algo mude no tribunal da Apple).
Ben Kreeger
16

Eu estava procurando por isso e encontrei a resposta de Richard Venable. Funciona para mim.

O iOS 5 inclui um novo método no UITableView: registerNib: forCellReuseIdentifier:

Para usá-lo, coloque um UITableViewCell em uma ponta. Ele deve ser o único objeto raiz na ponta.

Você pode registrar a ponta depois de carregar o tableView e, quando chamar dequeueReusableCellWithIdentifier: com o identificador de célula, ela será puxada da ponta, como se você tivesse usado uma célula de protótipo do Storyboard.

Odrakir
fonte
10

BJ Homer deu uma excelente explicação sobre o que está acontecendo.

Do ponto de vista prático, eu acrescentaria que, como você não pode ter células como xibs E conectar sequências, a melhor opção é ter a célula como xib - as transições são muito mais fáceis de manter do que os layouts e as propriedades das células em vários lugares , e seus seguidores provavelmente serão diferentes de seus diferentes controladores de qualquer maneira. Você pode definir o segue diretamente do seu controlador de exibição de tabela para o próximo controlador e executá-lo no código. .

Uma observação adicional é que ter o seu celular como um arquivo xib separado impede que você seja capaz de conectar quaisquer ações etc. diretamente ao controlador de exibição de tabela (eu não resolvi isso de qualquer maneira - você não pode definir o proprietário do arquivo como algo significativo ) Estou contornando isso definindo um protocolo ao qual o controlador de exibição de tabela da célula deve estar em conformidade e adicionando o controlador como uma propriedade fraca, semelhante a um delegado, no cellForRowAtIndexPath.

jrturton
fonte
10

Swift 3

BJ Homer deu uma excelente explicação: isso me ajuda a entender o conceito. Para make a custom cell reusable in storyboard, que pode ser usado em qualquer TableViewController, temos que nos mix the Storyboard and xibaproximar. Suponha que tenhamos uma célula nomeada como CustomCellqual deve ser usada no TableViewControllerOnee TableViewControllerTwo. Eu estou fazendo isso em etapas.
1. Arquivo> Novo> Clique em Arquivo> Selecionar Classe do Cocoa Touch> clique em Avançar> Nome da sua classe (por exemplo CustomCell)> selecione Subclasse como UITableVieCell> Marque a caixa de seleção também criar arquivo XIB e pressione Avançar.
2. Personalize a célula como desejar e defina o identificador no inspetor de atributos para célula, aqui definiremos como CellIdentifier. Esse identificador será usado no seu ViewController para identificar e reutilizar a célula.
3. Agora só temos queregister this cellno nosso ViewController viewDidLoad. Não há necessidade de nenhum método de inicialização.
4. Agora podemos usar essa célula personalizada em qualquer tableView.

Em TableViewControllerOne

let reuseIdentifier = "CellIdentifier"

override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UINib(nibName: "CustomCell", bundle: nil), forCellReuseIdentifier: reuseIdentifier)
} 

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier:reuseIdentifier, for: indexPath) as! CustomCell
    return cell!
}
Kunal Kumar
fonte
5

Eu encontrei uma maneira de carregar a célula para o mesmo VC, não testado para os seguintes. Isso pode ser uma solução alternativa para criar a célula em uma ponta separada

Digamos que você tenha uma VC e 2 tabelas e deseja criar uma célula no storyboard e usá-la nas duas tabelas.

(por exemplo: uma tabela e um campo de pesquisa com um UISearchController com uma tabela para resultados e você deseja usar a mesma célula em ambos)

Quando o controlador solicitar a célula, faça o seguinte:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString * identifier = @"CELL_ID";

    ContactsCell *cell = [self.YOURTABLEVIEW dequeueReusableCellWithIdentifier:identifier];
  // Ignore the "tableView" argument
}

E aqui você tem seu celular do storyboard

João Nunes
fonte
Eu tentei isso e parece funcionar, no entanto, as células nunca são reutilizadas. O sistema sempre cria e desaloca novas células a cada vez.
GilroyKilroy
Essa é a mesma recomendação que pode ser encontrada em Adicionando uma barra de pesquisa a uma exibição de tabela com storyboards . Se você estiver interessado, há uma explicação mais aprofundada dessa solução (procure tableView:cellForRowAtIndexPath:).
Senseful
mas isso é menos texto e responde à pergunta
João Nunes