Wie vermeide ich großen und plumpen UITableViewController unter iOS?

36

Ich habe ein Problem bei der Implementierung des MVC-Musters unter iOS. Ich habe im Internet gesucht, aber anscheinend keine gute Lösung für dieses Problem gefunden.

Viele UITableViewControllerImplementierungen scheinen ziemlich umfangreich zu sein. Die meisten Beispiele, die ich gesehen habe, lassen das UITableViewControllerumsetzen <UITableViewDelegate>und <UITableViewDataSource>. Diese Implementierungen sind ein wichtiger Grund, warum UITableViewControlleres immer größer wird. Eine Lösung wäre, separate Klassen zu erstellen, die <UITableViewDelegate>und implementieren <UITableViewDataSource>. Natürlich müssten diese Klassen einen Verweis auf die UITableViewController. Gibt es Nachteile bei dieser Lösung? Im Allgemeinen denke ich, dass Sie die Funktionalität an andere "Helper" -Klassen oder ähnliches delegieren sollten, indem Sie das Delegierungsmuster verwenden. Gibt es etablierte Wege, um dieses Problem zu lösen?

Ich möchte nicht, dass das Modell zu viele Funktionen enthält, noch die Ansicht. Ich glaube, dass die Logik eigentlich in der Controller-Klasse liegen sollte, da dies einer der Eckpfeiler des MVC-Patterns ist. Aber die große Frage ist:

Wie soll man den Controller einer MVC-Implementierung in kleinere, überschaubare Teile aufteilen? (Gilt in diesem Fall für MVC unter iOS.)

Es könnte ein allgemeines Muster für die Lösung dieses Problems geben, obwohl ich speziell nach einer Lösung für iOS suche. Bitte geben Sie ein Beispiel für ein gutes Muster zur Lösung dieses Problems. Bitte geben Sie ein Argument an, warum Ihre Lösung fantastisch ist.

Johan Karlsson
quelle
1
"Auch ein Argument, warum diese Lösung großartig ist." :)
Occulus
1
Das ist ein bisschen UITableViewControllernebensächlich , aber die Mechanik scheint mir ziemlich seltsam, so dass ich mich auf das Problem beziehen kann. Ich bin eigentlich froh , dass ich Gebrauch MonoTouch, weil MonoTouch.Dialoges speziell macht , dass viel einfacher, die Arbeit mit Tabellen auf iOS. In der Zwischenzeit bin ich gespannt, was andere, sachkundigere Leute hier vorschlagen könnten ...
Patryk Ćwiek

Antworten:

43

Ich vermeide es zu benutzen UITableViewController, da es viele Verantwortlichkeiten in ein einzelnes Objekt legt. Deshalb trenne ich die UIViewControllerUnterklasse von der Datenquelle und delegiere. Der View Controller ist dafür verantwortlich, die Tabellenansicht vorzubereiten, eine Datenquelle mit Daten zu erstellen und diese Dinge miteinander zu verknüpfen. Die Darstellung der Tabellenansicht kann geändert werden, ohne dass der Ansichts-Controller geändert werden muss. In der Tat kann derselbe Ansichts-Controller für mehrere Datenquellen verwendet werden, die alle diesem Muster folgen. In ähnlicher Weise bedeutet das Ändern des App-Workflows Änderungen am Ansichts-Controller, ohne sich Gedanken darüber zu machen, was mit der Tabelle geschieht.

Ich habe versucht, das UITableViewDataSourceund UITableViewDelegate-Protokoll in verschiedene Objekte zu unterteilen, aber das führt normalerweise zu einer falschen Aufteilung, da fast jede Methode des Delegaten in die Datenquelle eintauchen muss (z. B. muss der Delegat bei der Auswahl wissen, welches Objekt durch das dargestellt wird ausgewählte Zeile). Am Ende habe ich ein einzelnes Objekt, das sowohl Datenquelle als auch Delegat ist. Dieses Objekt stellt immer eine Methode bereit, mit -(id)tableView: (UITableView *)tableView representedObjectAtIndexPath: (NSIndexPath *)indexPathder sowohl die Datenquelle als auch die Delegierungsaspekte wissen müssen, woran sie arbeiten.

Das ist meine "Level 0" Trennung von Bedenken. Stufe 1 wird aktiviert, wenn ich Objekte unterschiedlicher Art in derselben Tabellenansicht darstellen muss. Stellen Sie sich zum Beispiel vor, Sie müssten die Kontakte-App schreiben - für einen einzelnen Kontakt stehen möglicherweise Zeilen für Telefonnummern, andere Zeilen für Adressen, andere für E-Mail-Adressen usw. Ich möchte diesen Ansatz vermeiden:

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

Bisher haben sich zwei Lösungen vorgestellt. Eine besteht darin, einen Selektor dynamisch zu konstruieren:

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

Bei diesem Ansatz müssen Sie den Epic- if()Baum nicht bearbeiten , um einen neuen Typ zu unterstützen. Fügen Sie einfach die Methode hinzu, die die neue Klasse unterstützt. Dies ist ein guter Ansatz, wenn diese Tabellenansicht die einzige ist, die diese Objekte darstellen oder auf besondere Weise darstellen muss. Wenn die gleichen Objekte in unterschiedlichen Tabellen mit unterschiedlichen Datenquellen dargestellt werden sollen, schlägt dieser Ansatz fehl, da die Zellenerstellungsmethoden über die Datenquellen verteilt werden müssen. Sie können eine gemeinsame Superklasse definieren, die diese Methoden bereitstellt, oder dies tun:

@interface PhoneNumber (TableViewRepresentation)

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

@end

@interface Address (TableViewRepresentation)

//more of the same…

@end

Dann in Ihrer Datenquellenklasse:

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

Dies bedeutet, dass jede Datenquelle, die Telefonnummern, Adressen usw. anzeigen muss, nur das Objekt abfragen kann, das für eine Tabellensichtzelle dargestellt wird. Die Datenquelle selbst muss nichts mehr über das angezeigte Objekt wissen.

„Aber warten“ , höre ich eine hypothetische Gesprächspartner einwerfen, „nicht , dass Pause MVC? Sind nicht Sie Details in eine Modellklasse setzen?“

Nein, MVC wird nicht beschädigt. Sie können sich die Kategorien in diesem Fall als eine Implementierung von Decorator vorstellen . Dies PhoneNumberist eine Modellklasse, aber PhoneNumber(TableViewRepresentation)eine Ansichtskategorie. Die Datenquelle (ein Controller-Objekt) vermittelt zwischen dem Modell und der Ansicht, sodass die MVC-Architektur weiterhin gültig ist.

Sie können diese Verwendung von Kategorien auch als Dekoration in Apples Frameworks sehen. NSAttributedStringist eine Modellklasse, die Text und Attribute enthält. AppKit NSAttributedString(AppKitAdditions)und UIKit bieten NSAttributedString(NSStringDrawing)Dekoratorkategorien, die diesen Modellklassen das Zeichenverhalten hinzufügen.


quelle
Was ist ein guter Name für die Klasse, die als Datenquellen- und Tabellenansichtsdelegat fungiert?
Johan Karlsson
1
@ JohanKarlsson Ich nenne es oft nur die Datenquelle. Es ist vielleicht ein bisschen schlampig, aber ich kombiniere die beiden oft genug, um zu wissen, dass meine "Datenquelle" eine Anpassung an Apples eingeschränktere Definition ist.
1
In diesem Artikel: objc.io/issue-1/table-views.html wird eine Möglichkeit zum Behandeln mehrerer Zelltypen vorgeschlagen, bei der Sie die Zellklasse in der cellForPhotoAtIndexPathMethode der Datenquelle ausarbeiten und dann eine entsprechende Factory-Methode aufrufen. Was natürlich nur möglich ist, wenn bestimmte Klassen vorhersehbar bestimmte Zeilen belegen. Ich denke, Ihr System zur Erzeugung von Kategorien nach Modell ist in der Praxis viel eleganter, obwohl es vielleicht eine unorthodoxe Herangehensweise an MVC ist! :)
Benji XVI
1
Ich habe versucht, dieses Muster unter github.com/yonglam/TableViewPattern zu testen . Hoffe, es ist nützlich für jemanden.
Andrew
1
Ich werde ein definitives Nein für den Ansatz der dynamischen Auswahl geben. Dies ist sehr gefährlich, da die Probleme nur zur Laufzeit auftreten. Es gibt keine automatisierte Möglichkeit, um sicherzustellen, dass der angegebene Selektor vorhanden ist und korrekt eingegeben wurde, und diese Art von Ansatz wird irgendwann auseinanderfallen und ist ein Alptraum, den es zu bewahren gilt. Der andere Ansatz ist jedoch sehr clever.
mkko
3

Leute neigen dazu, viel in den UIViewController / UITableViewController zu packen.

Die Delegierung an eine andere Klasse (nicht den View Controller) funktioniert normalerweise einwandfrei. Die Delegierten benötigen nicht unbedingt einen Verweis zurück auf den Ansichtscontroller, da alle Delegiertenmethoden einen Verweis auf den übergeben bekommen UITableView, aber sie benötigen irgendwie Zugriff auf die Daten, für die sie delegieren.

Einige Ideen für eine Neuorganisation, um die Länge zu reduzieren:

  • Wenn Sie die Zellen der Tabellenansicht im Code erstellen, sollten Sie sie stattdessen aus einer NIB-Datei oder einem Storyboard laden. In Storyboards sind Prototyp- und statische Tabellenzellen möglich. Wenn Sie nicht vertraut sind, überprüfen Sie diese Funktionen

  • Wenn Ihre Delegate-Methoden viele "if" -Anweisungen (oder switch -Anweisungen) enthalten, ist dies ein klassisches Zeichen dafür, dass Sie Refactoring durchführen können

Es fühlte sich für mich immer ein bisschen komisch an, dass der UITableViewDataSourcedafür verantwortlich war, das richtige Datenbit in den Griff zu bekommen und eine Ansicht zu konfigurieren, um es zu zeigen. Ein netter Refactoring-Punkt könnte sein, Ihre zu ändern cellForRowAtIndexPath, um ein Handle auf die Daten zu bekommen, die in einer Zelle angezeigt werden müssen, und dann die Erstellung der Zellenansicht an einen anderen Delegaten zu delegieren (z. B. ein CellViewDelegateoder ähnliches), der in dem entsprechenden Datenelement übergeben wird.

Occulus
quelle
Das ist eine schöne Antwort. Allerdings tauchen in meinem Kopf ein paar Fragen auf. Warum finden Sie viele if-Anweisungen (oder switch-Anweisungen) schlechtes Design? Meinen Sie eigentlich viele verschachtelte if- und switch-Anweisungen? Wie kann man umrechnen, um If- oder Switch-Anweisungen zu vermeiden?
Johan Karlsson
@JohanKarlsson Eine Technik ist der Polymorphismus. Wenn Sie eines mit einem Objekttyp und etwas anderes mit einem anderen Typ tun müssen, müssen Sie diese Objekte in verschiedene Klassen unterteilen und sie die Arbeit für Sie auswählen lassen.
@ AbrahamLee Ja, ich kenne Polymorphismus ;-) Allerdings bin ich mir nicht sicher, wie ich es in diesem Zusammenhang anwenden soll. Bitte erläutern Sie dies.
Johan Karlsson
@JohanKarlsson fertig;)
2

Hier ist ungefähr, was ich gerade mache, wenn ich mit einem ähnlichen Problem konfrontiert bin:

  • Verschieben Sie datenbezogene Operationen in die XXXDataSource-Klasse (die von BaseDataSource: NSObject erbt). BaseDataSource stellt einige praktische Methoden zur Verfügung, z. B. das - (NSUInteger)rowsInSection:(NSUInteger)sectionNum;Überschreiben von Daten durch Unterklassen (da Apps normalerweise eine Art Offie-Cache-Lademethode verwenden - (void)loadDataWithUpdateBlock:(LoadProgressBlock)dataLoadBlock completion:(LoadCompletionBlock)completionBlock;, mit der die Benutzeroberfläche mit den in LoadProgressBlock empfangenen zwischengespeicherten Daten aktualisiert werden kann, während Informationen aus dem Netzwerk aktualisiert werden, und der Abschlussblock Wir aktualisieren die Benutzeroberfläche mit neuen Daten und entfernen etwaige Fortschrittsindikatoren. Diese Klassen entsprechen NICHT dem UITableViewDataSourceProtokoll.

  • In BaseTableViewController (der mit UITableViewDataSourceund UITableViewDelegateProtokollen konform ist ) verweise ich auf BaseDataSource, die ich während der Initialisierung des Controllers erstellt habe. In einem UITableViewDataSourceTeil des Controllers gebe ich einfach Werte von dataSource zurück (wie - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return [self.tableViewDataSource sectionsCount]; }).

Hier ist mein cellForRow in der Basisklasse (kein Überschreiben in Unterklassen erforderlich):

- (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 muss von Unterklassen überschrieben werden und createCell gibt UITableViewCell zurück. Wenn Sie also eine benutzerdefinierte Zelle möchten, überschreiben Sie diese ebenfalls.

  • Nachdem die Basis-Dinge konfiguriert wurden (tatsächlich kann dieser Teil in einem ersten Projekt, das ein solches Schema verwendet, wiederverwendet werden), bleibt für BaseTableViewControllerUnterklassen Folgendes übrig :

    • Überschreiben von configureCell (dies führt normalerweise dazu, dass dataSource nach dem Objekt für den Indexpfad gefragt und an die configureWithXXX: -Methode der Zelle weitergeleitet wird oder die UITableViewCell-Darstellung des Objekts abgerufen wird, wie in der Antwort von user4051 angegeben)

    • DidSelectRowAtIndexPath überschreiben: (offensichtlich)

    • Schreiben Sie eine BaseDataSource-Unterklasse, die sich um die Arbeit mit dem erforderlichen Teil des Modells kümmert (angenommen, es gibt 2 Klassen Accountund die LanguageUnterklassen sind AccountDataSource und LanguageDataSource).

Und das ist alles für die Tabellenansicht. Ich kann bei Bedarf Code an GitHub senden.

Bearbeiten: Einige Empfehlungen finden Sie unter http://www.objc.io/issue-1/lighter-view-controllers.html (mit Link zu dieser Frage) und im Begleitartikel zu Tableviewcontrollers.

Timur Kuchkarov
quelle
2

Meiner Ansicht nach muss das Modell ein Array von Objekten enthalten, die ViewModel oder viewData heißen und in einem cellConfigurator gekapselt sind. Der CellConfigurator enthält die CellInfo, die zum Dequeen und Konfigurieren der Zelle erforderlich ist. Es gibt der Zelle einige Daten, so dass die Zelle sich selbst konfigurieren kann. Dies funktioniert auch mit section, wenn Sie ein SectionConfigurator-Objekt hinzufügen, das die CellConfigurators enthält. Ich habe vor einiger Zeit damit begonnen, der Zelle zunächst nur viewData zu geben, und der ViewController hat sich mit dem Entfernen der Zelle befasst. aber ich habe einen Artikel gelesen, der auf dieses gitHub-Repo verweist.

https://github.com/fastred/ConfigurableTableViewController

Dies kann die Herangehensweise ändern.

Pascale Beaulac
quelle
2

Ich habe kürzlich einen Artikel über die Implementierung von Delegates und Datenquellen für UITableView geschrieben: http://gosuwachu.gitlab.io/2014/01/12/uitableview-controller/

Die Hauptidee besteht darin, die Zuständigkeiten in separate Klassen wie Cell Factory und Section Factory aufzuteilen und eine allgemeine Schnittstelle für das Modell bereitzustellen, das UITableView anzeigen soll. Das folgende Diagramm erklärt alles:

Bildbeschreibung hier eingeben

Piotr Wach
quelle
Dieser Link funktioniert nicht mehr.
Koen
1

Das Befolgen der SOLID- Prinzipien löst alle derartigen Probleme.

Wenn Sie Ihre Klassen haben wollen nur eine einzige Verantwortung, sollten Sie separate definieren DataSourceund DelegateKlassen und einfach injizieren sie dem tableViewEigentümer (könnte UITableViewControlleroder UIViewControlleroder etwas anderes). Auf diese Weise überwinden Sie die Trennung von Bedenken .

Aber wenn Sie nur sauberen und lesbaren Code haben und diese riesige viewController- Datei loswerden möchten und sich in Swif befinden , können Sie dafür extensions verwenden. Erweiterungen der einzelnen Klasse können in verschiedene Dateien geschrieben werden und alle haben Zugriff aufeinander. Aber dies ist keine wirkliche Lösung für das SoC- Problem, wie ich bereits erwähnte.

Mojtaba Hosseini
quelle