Come evitare UITableViewController grande e goffo su iOS?


36

Ho un problema durante l'implementazione del modello MVC su iOS. Ho cercato su Internet, ma sembra non trovare alcuna buona soluzione a questo problema.

Molte UITableViewControllerimplementazioni sembrano essere piuttosto grandi. La maggior parte degli esempi che ho visto consente di UITableViewControllerimplementare <UITableViewDelegate>e <UITableViewDataSource>. Queste implementazioni sono un grande motivo per cui UITableViewControllersta diventando grande. Una soluzione sarebbe quella di creare classi separate che implementano <UITableViewDelegate>e <UITableViewDataSource>. Naturalmente queste classi dovrebbero avere un riferimento alUITableViewController . Ci sono degli svantaggi nell'usare questa soluzione? In generale, penso che dovresti delegare la funzionalità ad altre classi "Helper" o simili, usando il modello delegato. Esistono modi ben definiti per risolvere questo problema?

Non voglio che il modello contenga troppe funzionalità, né la vista. Credo che la logica dovrebbe davvero essere nella classe controller, poiché questo è uno dei cardini del modello MVC. Ma la grande domanda è:

Come dovresti dividere il controller di un'implementazione MVC in parti gestibili più piccole? (Vale per MVC in iOS in questo caso)

Potrebbe esserci un modello generale per risolverlo, anche se sto specificatamente cercando una soluzione per iOS. Fornisci un esempio di un modello valido per risolvere questo problema. Fornisci un argomento sul perché la tua soluzione è eccezionale.


1
"Anche un argomento per cui questa soluzione è fantastica." :)
occulus

1
Questo è un po 'fuori punto, ma la UITableViewControllermeccanica mi sembra abbastanza strana, quindi posso collegarmi al problema. Sono davvero contento che l'uso MonoTouch, a causa MonoTouch.Dialogin particolare rende che molto più facile lavorare con le tabelle su iOS. Nel frattempo, sono curioso di sapere quali altre persone più competenti potrebbero suggerire qui ...
Patryk Ćwiek,

Risposte:


43

Evito di usare UITableViewController, in quanto mette molte responsabilità in un singolo oggetto. Pertanto separo la UIViewControllersottoclasse dall'origine dati e dal delegato. La responsabilità del controller della vista è preparare la vista della tabella, creare un'origine dati con i dati e collegare tali elementi insieme. La modifica del modo in cui viene rappresentata la vista tabella può essere eseguita senza modificare il controller di visualizzazione e in effetti lo stesso controller di visualizzazione può essere utilizzato per più origini dati che seguono tutti questo schema. Allo stesso modo, cambiare il flusso di lavoro dell'app significa cambiare il controller di visualizzazione senza preoccuparsi di ciò che accade alla tabella.

Ho provato a separare i protocolli UITableViewDataSourcee UITableViewDelegatein diversi oggetti, ma di solito finisce per essere una falsa divisione poiché quasi tutti i metodi sul delegato devono scavare nell'origine dati (ad esempio, sulla selezione, il delegato deve sapere quale oggetto è rappresentato dal riga selezionata). Quindi finisco con un singolo oggetto che è sia l'origine dati che il delegato. Questo oggetto fornisce sempre un metodo su -(id)tableView: (UITableView *)tableView representedObjectAtIndexPath: (NSIndexPath *)indexPathcui sia l'origine dati che gli aspetti delegati devono sapere su cosa stanno lavorando.

Questa è la mia separazione del livello "0" delle preoccupazioni. Il livello 1 si impegna se devo rappresentare oggetti di diverso tipo nella stessa vista tabella. Ad esempio, immagina di dover scrivere l'app Contatti: per un singolo contatto, potresti avere righe che rappresentano numeri di telefono, altre righe che rappresentano indirizzi, altre che rappresentano indirizzi e-mail e così via. Voglio evitare questo approccio:

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

Finora si sono presentate due soluzioni. Uno è quello di costruire dinamicamente un selettore:

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

In questo approccio, non è necessario modificare l' if()albero epico per supportare un nuovo tipo: basta aggiungere il metodo che supporta la nuova classe. Questo è un ottimo approccio se questa vista tabella è l'unica che deve rappresentare questi oggetti o deve presentarli in un modo speciale. Se gli stessi oggetti saranno rappresentati in tabelle diverse con origini dati diverse, questo approccio si interrompe poiché i metodi di creazione delle celle devono essere condivisi tra le origini dati: è possibile definire una superclasse comune che fornisce questi metodi oppure è possibile farlo:

@interface PhoneNumber (TableViewRepresentation)

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

@end

@interface Address (TableViewRepresentation)

//more of the same…

@end

Quindi nella tua classe di origine dati:

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

Ciò significa che qualsiasi origine dati che deve visualizzare numeri di telefono, indirizzi ecc. Può semplicemente chiedere qualsiasi oggetto rappresentato per una cella di visualizzazione tabella. L'origine dati stessa non deve più sapere nulla sull'oggetto visualizzato.

"Ma aspetta," sento interporre un ipotetico interlocutore, "questo non rompe MVC? Non stai mettendo i dettagli di visualizzazione in una classe modello?"

No, non rompe MVC. Puoi pensare alle categorie in questo caso come un'implementazione di Decorator ; quindi PhoneNumberè una classe di modello ma PhoneNumber(TableViewRepresentation)è una categoria di visualizzazione. L'origine dati (un oggetto controller) media tra il modello e la vista, quindi l'architettura MVC è ancora valida.

Puoi vedere questo uso delle categorie come decorazione anche nei framework di Apple. NSAttributedStringè una classe del modello, con testo e attributi. AppKit fornisce NSAttributedString(AppKitAdditions)e UIKit fornisce NSAttributedString(NSStringDrawing)categorie di decoratori che aggiungono il comportamento del disegno a queste classi di modelli.


Qual è un buon nome per la classe che funziona come delegato dell'origine dati e della vista tabella?
Johan Karlsson,

1
@JohanKarlsson Spesso lo chiamo solo fonte di dati. Forse è un po 'sciatto, ma unisco i due abbastanza spesso per sapere che la mia "fonte di dati" è un adattamento alla definizione più limitata di Apple.

1
Questo articolo: objc.io/issue-1/table-views.html propone un modo per gestire più tipi di celle mediante i quali elaborare la classe di celle nel cellForPhotoAtIndexPathmetodo dell'origine dati, quindi chiamare un metodo factory appropriato. Il che ovviamente è possibile solo se determinate classi occupano prevedibilmente righe particolari. Il tuo sistema di categorie su modelli che generano visualizzazioni è molto più elegante in pratica, penso, sebbene sia forse un approccio non ortodosso a MVC! :)
Benji XVI,

1
Ho provato a provare questo schema su github.com/yonglam/TableViewPattern . Spero sia utile per qualcuno.
Andrew,

1
Voterò un no definitivo per l'approccio del selettore dinamico. È molto pericoloso poiché i problemi si manifestano solo in fase di esecuzione. Non esiste un modo automatico per assicurarsi che il selettore in questione esista e che sia stato digitato correttamente e che questo tipo di approccio alla fine cada a pezzi ed è un incubo da mantenere. L'altro approccio, tuttavia, è molto intelligente.
mkko,

3

Le persone tendono a impacchettarsi molto in UIViewController / UITableViewController.

La delega in un'altra classe (non il controller di visualizzazione) di solito funziona bene. I delegati non necessitano necessariamente di un riferimento al controller della vista, poiché a tutti i metodi delegati viene passato un riferimento a UITableView, ma dovranno in qualche modo accedere ai dati per i quali stanno delegando.

Alcune idee per la riorganizzazione per ridurre la lunghezza:

  • se stai costruendo le celle della vista tabella nel codice, considera di caricarle invece da un file pennino o da uno storyboard. Gli storyboard consentono il prototipo e le celle di tabelle statiche: controlla queste funzionalità se non conosci

  • se i tuoi metodi delegati contengono molte istruzioni "if" (o istruzioni switch) è un classico segno che puoi eseguire un refactoring

Mi è sempre sembrato un po 'strano che UITableViewDataSourcefosse responsabile di ottenere un controllo sul bit corretto di dati e di configurare una vista per mostrarlo. Un buon punto di refactoring potrebbe essere quello di cambiare il tuo cellForRowAtIndexPathper ottenere un handle sui dati che devono essere visualizzati in una cella, quindi delegare la creazione della vista della cella a un altro delegato (ad es. Crea uno CellViewDelegateo simile) che viene passato nell'elemento dati appropriato.


Questa è una bella risposta Tuttavia, un paio di domande sorgono nella mia testa. Perché trovi molte dichiarazioni if ​​(o switch-statement) come cattive progettazioni? Intendi davvero molte dichiarazioni if ​​e switch annidate? Come riesaminare per evitare le dichiarazioni if ​​o switch?
Johan Karlsson,

@JohanKarlsson una tecnica è attraverso il polimorfismo. Se devi fare una cosa con un tipo di oggetto e qualcos'altro con un tipo diverso, rendi tali oggetti classi diverse e lascia che scelgano il lavoro per te.

@GrahamLee Sì, conosco il polimorfismo ;-) Tuttavia non sono sicuro di come applicarlo in questo contesto. Si prega di approfondire questo.
Johan Karlsson,

@JohanKarlsson done;)

2

Ecco all'incirca quello che sto facendo attualmente di fronte a un problema simile:

  • Spostare le operazioni relative ai dati nella classe XXXDataSource (che eredita da BaseDataSource: NSObject). BaseDataSource fornisce alcuni metodi utili come la - (NSUInteger)rowsInSection:(NSUInteger)sectionNum;sottoclasse sostituisce il metodo di caricamento dei dati (poiché le app di solito hanno una sorta di metodo di caricamento della cache offlie simile a - (void)loadDataWithUpdateBlock:(LoadProgressBlock)dataLoadBlock completion:(LoadCompletionBlock)completionBlock;quello che possiamo aggiornare l'interfaccia utente con i dati memorizzati nella cache ricevuti in LoadProgressBlock mentre stiamo aggiornando le informazioni dalla rete e nel blocco di completamento aggiorniamo l'interfaccia utente con nuovi dati e rimuoviamo eventuali indicatori di progressione). Queste classi NON sono conformi al UITableViewDataSourceprotocollo.

  • In BaseTableViewController (che è conforme UITableViewDataSourcee UITableViewDelegateprotocolli) ho riferimento a BaseDataSource, che creo durante l'iniz del controller. In UITableViewDataSourceparte del controller restituisco semplicemente i valori da dataSource (come - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return [self.tableViewDataSource sectionsCount]; }).

Ecco il mio cellForRow nella classe base (non è necessario eseguire l'override nelle sottoclassi):

- (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 essere sostituito da sottoclassi e createCell restituisce UITableViewCell, quindi se vuoi una cella personalizzata, sovrascrivi anche quella.

  • Dopo che le cose di base sono configurate (in realtà, nel primo progetto che utilizza tale schema, dopo che questa parte può essere riutilizzata) ciò che rimane per le BaseTableViewControllersottoclassi è:

    • Sostituisci configureCell (questo di solito si trasforma in chiedere a DataSource l'oggetto per il percorso dell'indice e alimentarlo nella configurazioneWithXXX della cella: metodo o ottenere la rappresentazione UITableViewCell dell'oggetto come nella risposta di user4051)

    • Sostituisci didSelectRowAtIndexPath: (ovviamente)

    • Scrivi la sottoclasse BaseDataSource che si occupa di lavorare con la parte necessaria del modello (supponi che ci siano 2 classi Accounte Language, quindi le sottoclassi saranno AccountDataSource e LanguageDataSource).

E questo è tutto per la parte vista tabella. Posso pubblicare un po 'di codice su GitHub se necessario.

Modifica: alcune raccomandazioni sono disponibili su http://www.objc.io/issue-1/lighter-view-controllers.html (che ha un link a questa domanda) e un articolo di accompagnamento su tableviewcontroller.


2

La mia opinione su questo è che il modello deve fornire una matrice di oggetti che sono chiamati ViewModel o viewData incapsulati in un cellConfigurator. CellConfigurator contiene il CellInfo necessario per rimuoverlo e configurare la cella. fornisce alcuni dati alla cella in modo che la cella possa configurarsi da sola. questo funziona anche con la sezione se aggiungi qualche oggetto SectionConfigurator che contiene CellConfigurators. Ho iniziato a usarlo poco fa inizialmente solo dando alla cellula un viewData e avevo il ViewController occuparsi di dequeuare la cellula. ma ho letto un articolo che indicava questo repository gitHub.

https://github.com/fastred/ConfigurableTableViewController

questo potrebbe cambiare il modo in cui ti stai avvicinando.


2

Di recente ho scritto un articolo su come implementare delegati e fonti di dati per UITableView: http://gosuwachu.gitlab.io/2014/01/12/uitableview-controller/

L'idea principale è quella di dividere le responsabilità in classi separate, come cell factory, section factory, e fornire un'interfaccia generica per il modello che UITableView mostrerà. Lo schema seguente spiega tutto:

inserisci qui la descrizione dell'immagine


Questo link non funziona più.
Koen,

1

Seguire i principi SOLID risolverà qualsiasi tipo di problema come questi.

Se si desidera che le proprie classi abbiano SOLO UNA SINGOLA responsabilità, è necessario definire separate DataSourcee Delegateclassi e semplicemente iniettarle al tableViewproprietario (potrebbe essere UITableViewControllero UIViewControllero altro). Ecco come superare la separazione delle preoccupazioni .

Ma se vuoi solo avere un codice pulito e leggibile e vuoi sbarazzarti di quel massiccio file viewController e sei in Swif , puoi usare extensions per quello. Le estensioni della singola classe possono essere scritte in file diversi e tutti hanno accesso l'uno all'altro. Ma questo è davvero risolve il problema del SoC , come ho già detto.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.