Come evitare UITableViewController grande e maldestro su iOS?

35

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

Molte implementazioni di UITableViewController sembrano essere piuttosto grandi. La maggior parte degli esempi che ho visto consente a UITableViewController di implementare <UITableViewDelegate> e <UITableViewDataSource> . Queste implementazioni sono una grande ragione per cui UITableViewController sta diventando grande. Una soluzione sarebbe creare classi separate che implementino <UITableViewDelegate> e <UITableViewDataSource> . Ovviamente queste classi dovrebbero avere un riferimento al UITableViewController . Ci sono degli svantaggi nell'usare questa soluzione? In generale, penso che dovresti delegare la funzionalità ad altre classi "Helper" o simili, usando il pattern 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 essere nella classe controller, poiché questo è uno dei capisaldi del pattern MVC. Ma la grande domanda è:

Come dovresti dividere il controller di un'implementazione MVC in pezzi gestibili più piccoli? (Si applica a MVC in iOS in questo caso)

Potrebbe esserci un modello generale per risolvere questo problema, anche se sono specificamente alla ricerca di una soluzione per iOS. Si prega di dare un esempio di un buon modello per risolvere questo problema. Si prega di fornire un argomento perché la vostra soluzione è fantastica.

    
posta Johan Karlsson 29.11.2012 - 14:20
fonte

5 risposte

42

Evito di usare UITableViewController , poiché mette molte responsabilità in un singolo oggetto. Pertanto separo la sottoclasse UIViewController dall'origine dati e delegato. La responsabilità del controller di visualizzazione è di preparare la vista tabella, creare un'origine dati con dati e unirli insieme. Cambiando il modo in cui è rappresentata la vista tabella può essere fatto senza modificare il controller di visualizzazione, e infatti lo stesso controller di visualizzazione può essere utilizzato per più origini dati che seguono tutte questo schema. Allo stesso modo, la modifica del flusso di lavoro dell'app significa modifiche al controller della vista senza preoccuparsi di ciò che accade alla tabella.

Ho provato a separare i protocolli UITableViewDataSource e UITableViewDelegate in oggetti diversi, ma di solito si tratta di una falsa divisione poiché quasi ogni metodo sul delegato deve scavare nell'origine dati (ad esempio, sulla selezione, il delegato ha bisogno di sapere quale oggetto è rappresentato dalla riga selezionata). Quindi finisco con un singolo oggetto che è sia l'origine dati sia il delegato. Questo oggetto fornisce sempre un metodo -(id)tableView: (UITableView *)tableView representedObjectAtIndexPath: (NSIndexPath *)indexPath che sia l'aspetto dell'origine dati sia gli aspetti dei delegati devono sapere su cosa stanno lavorando.

Questa è la mia separazione di preoccupazioni di "livello 0". Il livello 1 viene attivato 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 email 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 …
}

Due soluzioni si sono presentate finora. 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'albero epico if() 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 verranno rappresentati in tabelle diverse con origini dati diverse, questo approccio si interromperà 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 effettuare questa operazione:

@interface PhoneNumber (TableViewRepresentation)

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

@end

@interface Address (TableViewRepresentation)

//more of the same…

@end

Quindi nella classe dell'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 fonte di dati che deve visualizzare numeri di telefono, indirizzi ecc. può solo chiedere qualunque oggetto è rappresentato per una cella di visualizzazione tabella. La fonte dei dati non ha più bisogno di sapere nulla sull'oggetto che viene visualizzato.

"Ma aspetta", sento un ipotetico interlocutore interlocutore, "non è che interrompe MVC? Non stai mettendo i dettagli della vista in una classe del modello?"

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

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

    
risposta data 04.12.2012 - 09:43
fonte
3

Le persone tendono a fare un sacco di cose su UIViewController / UITableViewController.

Di solito la delega a un'altra classe (non il controller di visualizzazione) funziona correttamente. I delegati non hanno necessariamente bisogno di un riferimento al controller della vista, poiché tutti i metodi delegati ricevono un riferimento a UITableView , ma in qualche modo avranno bisogno di accedere ai dati per cui stanno delegando.

Alcune idee per la riorganizzazione per ridurre la lunghezza:

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

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

Mi è sempre sembrato un po 'strano che il UITableViewDataSource fosse responsabile per ottenere un handle sul bit corretto di dati e per configurare una vista per mostrarlo. Un bel punto di refactoring potrebbe essere quello di cambiare il cellForRowAtIndexPath per ottenere un handle sui dati che devono essere visualizzati in una cella, quindi delegare la creazione della vista cella a un altro delegato (ad esempio, creare un CellViewDelegate o simile) che viene passato nell'elemento dati appropriato.

    
risposta data 30.11.2012 - 15:42
fonte
2

Ecco approssimativamente quello che sto facendo al momento di affrontare un problema simile:

  • Sposta le operazioni relative ai dati alla classe XXXDataSource (che eredita da BaseDataSource: NSObject). BaseDataSource fornisce alcuni metodi convenienti come - (NSUInteger)rowsInSection:(NSUInteger)sectionNum; , la sottoclasse sovrascrive il metodo di caricamento dei dati (dato che le app di solito hanno una sorta di metodo di caricamento della cache offlie come - (void)loadDataWithUpdateBlock:(LoadProgressBlock)dataLoadBlock completion:(LoadCompletionBlock)completionBlock; in modo che possiamo aggiornare l'interfaccia utente con i dati memorizzati in cache in LoadProgressBlock mentre stiamo aggiornando le informazioni da 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 protocollo UITableViewDataSource .

  • In BaseTableViewController (che è conforme ai protocolli UITableViewDataSource e UITableViewDelegate ) ho riferimento a BaseDataSource, che creo durante il controller init. Nella parte UITableViewDataSource del controller, restituisco semplicemente i valori da dataSource (come %codice%).

Ecco il mio cellForRow in classe base (non c'è bisogno di sovrascrivere in 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 sovrascritto dalle 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 sottoclassi di - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return [self.tableViewDataSource sectionsCount]; } è:

    • Sostituisci configureCell (questo di solito si trasforma in richiesta dataSource per l'oggetto per il percorso dell'indice e lo alimenta al configureWithXXX della cella: metodo o ottenendo la rappresentazione di UITableViewCell dell'oggetto come nella risposta di user4051)

    • Override didSelectRowAtIndexPath: (ovviamente)

    • Scrivi una sottoclasse BaseDataSource che si occupa di lavorare con la parte necessaria del Modello (supponiamo che ci siano 2 classi BaseTableViewController e Account , quindi le sottoclassi saranno AccountDataSource e LanguageDataSource).

E questo è tutto per la parte vista tabella. Posso pubblicare del codice su GitHub, se necessario.

Modifica: alcuni consigli possono essere trovati all'indirizzo link (che ha un link a questo domanda) e articolo associato su tableviewcontrollers.

    
risposta data 21.10.2013 - 20:26
fonte
2

La mia vista su questo è che il modello deve dare una matrice di oggetti che sono chiamati ViewModel o viewData incapsulati in un cellConfigurator. il CellConfigurator contiene il CellInfo necessario per dequearlo e per configurare la cella. fornisce alla cella alcuni dati in modo che la cella possa configurarsi autonomamente. questo funziona anche con la sezione se aggiungi qualche oggetto SectionConfigurator che contiene i CellConfigurator. Ho iniziato a utilizzare questo tempo fa inizialmente dando alla cella solo un viewData e ho avuto il controllo ViewController con il dequeuing della cella. ma ho letto un articolo che indicava questo repository gitHub.

link

questo potrebbe cambiare il modo in cui ti stai avvicinando a questo.

    
risposta data 29.04.2016 - 19:05
fonte
2

Recentemente ho scritto un articolo su come implementare i delegati e le origini dati per UITableView: link

L'idea principale è di suddividere le responsabilità in classi separate, come fabbrica di celle, fabbrica di sezioni e fornire un'interfaccia generica per il modello che UITableView sta per mostrare. Il diagramma seguente spiega tutto:

    
risposta data 17.01.2014 - 00:37
fonte

Leggi altre domande sui tag