Como evitar UITableViewController grande e desajeitado no iOS?

36

Estou com um problema ao implementar o padrão MVC no iOS. Eu procurei na Internet, mas parece não encontrar nenhuma solução legal para esse problema.

Muitas UITableViewControllerimplementações parecem ser bastante grandes. A maioria dos exemplos que eu vi permite UITableViewControllerimplementar <UITableViewDelegate>e <UITableViewDataSource>. Essas implementações são uma grande razão pela qual UITableViewControllerestá ficando grande. Uma solução seria criar classes separadas que implementam <UITableViewDelegate>e <UITableViewDataSource>. Claro que essas classes teriam que ter uma referência ao UITableViewController. Existem desvantagens ao usar esta solução? Em geral, acho que você deve delegar a funcionalidade para outras classes "Helper" ou similares, usando o padrão de delegação. Existem formas bem estabelecidas de resolver esse problema?

Não quero que o modelo contenha muita funcionalidade nem a visualização. Acredito que a lógica realmente deve estar na classe do controlador, pois esse é um dos pilares do padrão MVC. Mas a grande questão é:

Como você deve dividir o controlador de uma implementação MVC em partes gerenciáveis ​​menores? (Aplica-se ao MVC no iOS neste caso)

Pode haver um padrão geral para resolver isso, embora eu esteja procurando especificamente uma solução para iOS. Por favor, dê um exemplo de um bom padrão para resolver esse problema. Forneça um argumento sobre por que sua solução é incrível.

Johan Karlsson
fonte
1
"Também um argumento sobre por que essa solução é incrível." :)
occulus
1
Isso é um pouco irrelevante, mas a UITableViewControllermecânica me parece bastante estranha, para que eu possa me relacionar com o problema. Na verdade, estou feliz por usar MonoTouch, porque MonoTouch.Dialogespecificamente torna isso muito mais fácil trabalhar com tabelas no iOS. Enquanto isso, estou curioso para saber o que outras pessoas mais
instruídas

Respostas:

43

Evito usar UITableViewController, pois coloca muitas responsabilidades em um único objeto. Portanto, separo a UIViewControllersubclasse da fonte de dados e delego. A responsabilidade do controlador de exibição é preparar a exibição de tabela, criar uma fonte de dados com dados e conectar essas coisas. A alteração da maneira como a visualização da tabela é representada pode ser feita sem alterar o controlador de exibição e, de fato, o mesmo controlador de exibição pode ser usado para várias fontes de dados que seguem esse padrão. Da mesma forma, alterar o fluxo de trabalho do aplicativo significa alterações no controlador de exibição sem se preocupar com o que acontece com a tabela.

Eu tentei separar o UITableViewDataSourcee UITableViewDelegateprotocolos em diferentes objetos, mas que normalmente acaba por ser uma falsa divisão como quase todos os métodos para as necessidades de delegado para cavar a fonte de dados (por exemplo, na seleção, as necessidades de delegado para saber o que objeto é representado pela linha selecionada). Então, acabo com um único objeto que é a fonte de dados e o delegado. Este objeto sempre fornece um método-(id)tableView: (UITableView *)tableView representedObjectAtIndexPath: (NSIndexPath *)indexPath qual os aspectos da fonte de dados e do delegado precisam saber no que estão trabalhando.

Essa é a minha separação "nível 0" de preocupações. O nível 1 é ativado se eu tiver que representar objetos de tipos diferentes na mesma exibição de tabela. Como exemplo, imagine que você precise escrever o aplicativo Contatos - para um único contato, você pode ter linhas representando números de telefone, outras linhas representando endereços, outras representando endereços de email e assim por diante. Eu quero evitar essa abordagem:

- (UITableViewCell *)tableView: (UITableView *)tableView cellForRowAtIndexPath: (NSIndexPath *)indexPath {
  id object = [self tableView: tableView representedObjectAtIndexPath: indexPath];
  if ([object isKindOfClass: [PhoneNumber class]]) {
    //configure phone number cell
  }
  else if …
}

Duas soluções se apresentaram até agora. Uma é construir dinamicamente um seletor:

- (UITableViewCell *)tableView: (UITableView *)tableView cellForRowAtIndexPath: (NSIndexPath *)indexPath {
  id object = [self tableView: tableView representedObjectAtIndexPath: indexPath];
  NSString *cellSelectorName = [NSString stringWithFormat: @"tableView:cellFor%@AtIndexPath:", [object class]];
  SEL cellSelector = NSSelectorFromString(cellSelectorName);
  return [self performSelector: cellSelector withObject: tableView withObject: object];
}

- (UITableViewCell *)tableView: (UITableView *)tableView cellForPhoneNumberAtIndexPath: (NSIndexPath *)indexPath {
  // configure phone number cell
}

Nesta abordagem, você não precisa editar o épico if() árvore para oferecer suporte a um novo tipo - basta adicionar o método que suporta a nova classe. Essa é uma ótima abordagem se essa exibição de tabela for a única que precisa representar esses objetos ou precisa apresentá-los de uma maneira especial. Se os mesmos objetos serão representados em tabelas diferentes com fontes de dados diferentes, essa abordagem será interrompida conforme os métodos de criação de células precisem ser compartilhados entre as fontes de dados - você poderá definir uma superclasse comum que forneça esses métodos ou faça o seguinte:

@interface PhoneNumber (TableViewRepresentation)

- (UITableViewCell *)tableView: (UITableView *)tableView representationAsCellForRowAtIndexPath: (NSIndexPath *)indexPath;

@end

@interface Address (TableViewRepresentation)

//more of the same…

@end

Em seguida, na sua classe de fonte de dados:

- (UITableViewCell *)tableView: (UITableView *)tableView cellForRowAtIndexPath: (NSIndexPath *)indexPath {
  id object = [self tableView: tableView representedObjectAtIndexPath: indexPath];
  return [object tableView: tableView representationAsCellForRowAtIndexPath: indexPath];
}

Isso significa que qualquer fonte de dados que precise exibir números de telefone, endereços etc. pode apenas perguntar qualquer objeto representado para uma célula de exibição de tabela. A própria fonte de dados não precisa mais saber nada sobre o objeto que está sendo exibido.

"Mas espere", ouço um interlocutor hipotético interpor ", não é? quebra o MVC? Você não está colocando detalhes da exibição em uma classe de modelo?"

Não, não quebra o MVC. Você pode pensar nas categorias nesse caso como uma implementação do Decorator ; entãoPhoneNumber é uma classe de modelo, mas PhoneNumber(TableViewRepresentation)é uma categoria de exibição. A fonte de dados (um objeto do controlador) medeia entre o modelo e a visualização, portanto a arquitetura MVC ainda é válida.

Você pode ver esse uso de categorias como decoração também nas estruturas da Apple. NSAttributedStringé uma classe de modelo, contendo algum texto e atributos. O AppKit fornece NSAttributedString(AppKitAdditions)e o UIKit fornece NSAttributedString(NSStringDrawing)categorias de decorador que adicionam comportamento de desenho a essas classes de modelo.


fonte
Qual é um bom nome para a classe que está trabalhando como delegado de fonte de dados e exibição de tabela?
18713 Johan
1
@JohanKarlsson Costumo chamar isso de fonte de dados. É um pouco desleixado, talvez, mas eu combino os dois com frequência suficiente para saber que minha "fonte de dados" é uma adaptação à definição mais restrita da Apple.
1
Este artigo: objc.io/issue-1/table-views.html propõe uma maneira de lidar com vários tipos de células nas quais você trabalha a classe de célula no cellForPhotoAtIndexPathmétodo da fonte de dados e, em seguida, chama um método de fábrica apropriado. O que, obviamente, só é possível se determinadas classes ocuparem previsivelmente linhas específicas. Acho que seu sistema de geração de categorias em modelos é muito mais elegante na prática, embora seja talvez uma abordagem pouco ortodoxa para o MVC! :)
Benji XVI
1
Eu tentei demonstrar esse padrão em github.com/yonglam/TableViewPattern . Espero que seja útil para alguém.
Andrew
1
Vou votar um não definitivo para a abordagem do seletor dinâmico. É muito perigoso, pois os problemas só se manifestam no tempo de execução. Não há uma maneira automatizada de garantir que o seletor fornecido exista e que ele seja digitado corretamente, e esse tipo de abordagem acabará se desintegrando e é um pesadelo para manter. A outra abordagem, no entanto, é muito inteligente.
Mkko #
3

As pessoas tendem a incluir muito no UIViewController / UITableViewController.

A delegação para outra classe (não o controlador de exibição) geralmente funciona bem. Os delegados não precisam necessariamente de uma referência de volta ao controlador de exibição, pois todos os métodos de delegados passam uma referência aoUITableView , mas precisam acessar de alguma forma os dados para os quais estão delegando.

Algumas idéias para reorganização para reduzir o comprimento:

  • se você estiver construindo as células de exibição de tabela no código, considere carregá-las a partir de um arquivo de ponta ou de um storyboard. Os storyboards permitem células de tabela estáticas e protótipo - confira esses recursos se você não estiver familiarizado

  • se os métodos delegados contiverem muitas instruções 'if' (ou instruções de troca), isso é um sinal clássico de que você pode refatorar

Sempre me pareceu um pouco engraçado que o UITableViewDataSourceresponsável por controlar o bit correto de dados e configurar uma exibição para mostrá-lo. Um bom ponto de refatoração pode ser alterar o seu cellForRowAtIndexPathpara obter um controle sobre os dados que precisam ser exibidos em uma célula e delegar a criação da visualização da célula para outro delegado (por exemplo, criar um CellViewDelegateou similar) que é passado no item de dados apropriado.

occulus
fonte
Esta é uma boa resposta. No entanto, algumas perguntas surgem na minha cabeça. Por que você acha que muitas instruções if (ou instruções switch) são de design ruim? Você realmente quer dizer muitas declarações if e switch aninhadas? Como você fatoriza novamente para evitar declarações if ou switch?
Johan Karlsson
@JohanKarlsson uma técnica é via polimorfismo. Se você precisar fazer uma coisa com um tipo de objeto e outra com um tipo diferente, crie classes diferentes para esses objetos e deixe-os escolher o trabalho para você.
@GrahamLee Sim, eu sei polimorfismo ;-) No entanto, não sei como aplicá-lo nesse contexto. Por favor, elabore isso.
Johan Karlsson
@JohanKarlsson done;)
2

Aqui está aproximadamente o que estou fazendo atualmente ao enfrentar um problema semelhante:

  • Mova as operações relacionadas a dados para a classe XXXDataSource (que herda de BaseDataSource: NSObject). O BaseDataSource fornece alguns métodos convenientes, como a - (NSUInteger)rowsInSection:(NSUInteger)sectionNum;subclasse substitui o método de carregamento de dados (como os aplicativos geralmente têm algum tipo de método de carregamento de cache externo, - (void)loadDataWithUpdateBlock:(LoadProgressBlock)dataLoadBlock completion:(LoadCompletionBlock)completionBlock;para que possamos atualizar a interface do usuário com os dados em cache recebidos no LoadProgressBlock enquanto atualizamos as informações da rede e no bloco de conclusão atualizamos a interface do usuário com novos dados e removemos os indicadores de progresso, se houver). Essas classes NÃO estão em conformidade com o UITableViewDataSourceprotocolo.

  • Em BaseTableViewController (que se adapta à UITableViewDataSourcee UITableViewDelegateprotocolos) que tem a referência BaseDataSource, que crio durante a inicialização do controlador. Em UITableViewDataSourceparte do controlador, simplesmente retorno valores de dataSource (like - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return [self.tableViewDataSource sectionsCount]; }).

Aqui está o meu cellForRow na classe base (não é necessário substituir nas subclasses):

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    NSString *cellIdentifier = [NSString stringWithFormat:@"%@%@", NSStringFromClass([self class]), @"TableViewCell"];
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
    if (!cell) {
        cell = [self createCellForIndexPath:indexPath withCellIdentifier:cellIdentifier];
    }
    [self configureCell:cell atIndexPath:indexPath];
    return cell;
}

configureCell deve ser substituído por subclasses e createCell retorna UITableViewCell, portanto, se você desejar uma célula personalizada, substitua-a também.

  • Depois que as coisas básicas são configuradas (na verdade, no primeiro projeto que usa esse esquema, depois que essa parte pode ser reutilizada), o que resta para as BaseTableViewControllersubclasses é:

    • Substituir configureCell (isso geralmente se transforma em pedir ao dataSource o objeto para o caminho do índice e alimentá-lo com o método configureWithXXX: da célula ou obter a representação UITableViewCell do objeto, como na resposta do usuário4051)

    • Substituir didSelectRowAtIndexPath: (obviamente)

    • Escreva a subclasse BaseDataSource que cuida do trabalho com a parte necessária do Model (suponha que existam 2 classes Accounte Language, portanto, as subclasses serão AccountDataSource e LanguageDataSource).

E isso é tudo para ver parte da tabela. Posso postar algum código no GitHub, se necessário.

Edit: algumas recomendações podem ser encontradas em http://www.objc.io/issue-1/lighter-view-controllers.html (que tem um link para esta pergunta) e artigo complementar sobre os tableviewcontrollers.

Timur Kuchkarov
fonte
2

Minha opinião sobre isso é que o modelo precisa fornecer uma matriz de objetos chamados ViewModel ou viewData encapsulados em um cellConfigurator. o CellConfigurator mantém o CellInfo necessário para removê-lo e configurar a célula. ele fornece à célula alguns dados para que ela possa se configurar. isso também funciona com a seção se você adicionar algum objeto SectionConfigurator que mantenha os CellConfigurators. Comecei a usar isso há algum tempo, inicialmente, apenas fornecendo à célula um viewData e o ViewController lidava com o desenfileiramento da célula. mas li um artigo que apontava para este repositório do gitHub.

https://github.com/fastred/ConfigurableTableViewController

isso pode mudar a maneira como você está abordando isso.

Pascale Beaulac
fonte
2

Recentemente, escrevi um artigo sobre como implementar delegados e fontes de dados para o UITableView: http://gosuwachu.gitlab.io/2014/01/12/12/uitableview-controller/

A idéia principal é dividir responsabilidades em classes separadas, como fábrica de células, fábrica de seções e fornecer alguma interface genérica para o modelo que o UITableView exibirá. O diagrama abaixo explica tudo:

insira a descrição da imagem aqui

Piotr Wach
fonte
Este link não está mais funcionando.
Koen
1

Seguindo o SOLID princípios do resolverá qualquer tipo de problema como esse.

Se você quiser suas aulas de ter apenas uma única responsabilidade, você deve definir separada DataSourcee Delegateaulas e simplesmente injetar -los para o tableViewproprietário (poderia ser UITableViewControllerou UIViewControllerou qualquer outra coisa). É assim que você supera a separação de preocupações .

Mas se você quer apenas um código limpo e legível, e quer se livrar desse enorme arquivo viewController e está no Swif , pode usar extensions para isso. Extensões da classe única podem ser gravadas em arquivos diferentes e todas elas têm acesso uma à outra. Mas isso é nit resolve realmente o problema do SoC , como mencionei.

Mojtaba Hosseini
fonte