Wie erstelle ich in einem Storyboard eine benutzerdefinierte Zelle für die Verwendung mit mehreren Controllern?

216

Ich versuche, Storyboards in einer App zu verwenden, an der ich arbeite. In der App gibt es Listen und Benutzer, und jede enthält eine Sammlung der anderen (Mitglieder einer Liste, Listen, die einem Benutzer gehören). Also dementsprechend habe ich ListCellund UserCellKlassen. Das Ziel ist, dass diese in der gesamten App wiederverwendbar sind (dh in jedem meiner Tableview-Controller).

Dort stoße ich auf ein Problem.

Wie erstelle ich eine benutzerdefinierte Tabellenansichtszelle im Storyboard, die in jedem View Controller wiederverwendet werden kann?

Hier sind die spezifischen Dinge, die ich bisher ausprobiert habe.

  • Fügen Sie in Controller 1 eine Prototypzelle hinzu, setzen Sie die Klasse auf meine UITableViewCellUnterklasse, legen Sie die Wiederverwendungs-ID fest, fügen Sie die Beschriftungen hinzu und verdrahten Sie sie mit den Ausgängen der Klasse. Fügen Sie in Controller 2 eine leere Prototypzelle hinzu, setzen Sie sie auf dieselbe Klasse und verwenden Sie die ID wie zuvor wieder. Wenn es ausgeführt wird, werden die Beschriftungen nie angezeigt, wenn die Zellen in Controller 2 angezeigt werden. Funktioniert gut in Controller # 1.

  • Entwarf jeden Zelltyp in einer anderen NIB und verdrahtete ihn mit der entsprechenden Zellklasse. Fügen Sie im Storyboard eine leere Prototypzelle hinzu und legen Sie deren Klasse und Wiederverwendungs-ID fest, um auf meine Zellenklasse zu verweisen. viewDidLoadRegistrieren Sie in den Methoden der Controller diese NIB-Dateien für die Wiederverwendungs-ID. Wenn gezeigt, waren die Zellen in beiden Controllern wie der Prototyp leer.

  • Bewahrte Prototypen in beiden Controllern leeren und setzen die Klasse und verwenden die ID wieder für meine Zellenklasse. Konstruierte die Benutzeroberfläche der Zellen vollständig in Code. Zellen funktionieren perfekt in allen Controllern.

Im zweiten Fall vermute ich, dass der Prototyp die NIB immer überschreibt, und wenn ich die Prototypzellen tötete, würde die Registrierung meiner NIB für die Wiederverwendungs-ID funktionieren. Aber dann wäre ich nicht in der Lage, Segmente von den Zellen zu anderen Frames einzurichten, was wirklich der springende Punkt bei der Verwendung von Storyboards ist.

Letztendlich möchte ich zwei Dinge: Verkabeln von auf Tabellenansichten basierenden Flows im Storyboard und Definieren von Zellenlayouts visuell und nicht im Code. Ich kann bisher nicht sehen, wie ich beide bekommen kann.

Cliff W.
quelle

Antworten:

205

So wie ich es verstehe, möchten Sie:

  1. Entwerfen Sie eine Zelle in IB, die in mehreren Storyboard-Szenen verwendet werden kann.
  2. Konfigurieren Sie eindeutige Storyboard-Abschnitte von dieser Zelle, abhängig von der Szene, in der sich die Zelle befindet.

Leider gibt es derzeit keine Möglichkeit, dies zu tun. Um zu verstehen, warum Ihre vorherigen Versuche nicht funktioniert haben, müssen Sie mehr über die Funktionsweise von Storyboards und Prototyp-Tabellenansichtszellen wissen. (Wenn Sie sich nicht darum kümmern, warum diese anderen Versuche nicht funktioniert haben, können Sie jetzt gehen. Ich habe keine magischen Problemumgehungen für Sie, außer vorzuschlagen, dass Sie einen Fehler melden.)

Ein Storyboard ist im Wesentlichen nicht viel mehr als eine Sammlung von .xib-Dateien. Wenn Sie einen Table View Controller laden, der einige Prototypzellen aus einem Storyboard enthält, geschieht Folgendes:

  • Jede Prototypzelle ist tatsächlich eine eigene eingebettete Mini-Feder. Wenn der Table View Controller geladen wird, durchläuft er alle Schreibfedern und Aufrufe der Prototypzelle -[UITableView registerNib:forCellReuseIdentifier:].
  • Die Tabellenansicht fragt den Controller nach den Zellen.
  • Sie rufen wahrscheinlich an -[UITableView dequeueReusableCellWithIdentifier:]
  • Wenn Sie eine Zelle mit einer bestimmten Wiederverwendungskennung anfordern, wird geprüft, ob eine Feder registriert ist. Wenn dies der Fall ist, wird eine Instanz dieser Zelle instanziiert. Dies setzt sich aus folgenden Schritten zusammen:

    1. Sehen Sie sich die Klasse der Zelle an, wie sie in der Spitze der Zelle definiert ist. Rufen Sie an [[CellClass alloc] initWithCoder:].
    2. Die -initWithCoder:Methode führt Unteransichten durch und legt Eigenschaften fest, die in der Schreibfeder definiert wurden. ( IBOutlets werden wahrscheinlich auch hier angeschlossen, obwohl ich das nicht getestet habe; es kann in passieren -awakeFromNib)
  • Sie konfigurieren Ihre Zelle, wie Sie möchten.

Wichtig hierbei ist, dass zwischen der Klasse der Zelle und dem visuellen Erscheinungsbild der Zelle unterschieden wird. Sie können zwei separate Prototypzellen derselben Klasse erstellen, deren Unteransichten jedoch völlig unterschiedlich angeordnet sind. Wenn Sie die Standardstile UITableViewCellverwenden, geschieht genau dies. Der "Standard" -Stil und der "Untertitel" -Stil werden beispielsweise beide von derselben UITableViewCellKlasse dargestellt.

Dies ist wichtig : Die Klasse der Zelle hat keine Eins-zu-Eins-Korrelation mit einer bestimmten Ansichtshierarchie . Die Ansichtshierarchie wird vollständig durch das bestimmt, was sich in der Prototypzelle befindet, die bei diesem bestimmten Controller registriert wurde.

Beachten Sie auch, dass die Wiederverwendungskennung der Zelle in keiner globalen Zellapotheke registriert wurde. Die Wiederverwendungskennung wird nur im Kontext einer einzelnen UITableViewInstanz verwendet.


Schauen wir uns anhand dieser Informationen an, was bei Ihren obigen Versuchen passiert ist.

Fügen Sie in Controller 1 eine Prototypzelle hinzu, setzen Sie die Klasse auf meine UITableViewCell-Unterklasse, legen Sie die Wiederverwendungs-ID fest, fügen Sie die Beschriftungen hinzu und verdrahten Sie sie mit den Ausgängen der Klasse. Fügen Sie in Controller 2 eine leere Prototypzelle hinzu, setzen Sie sie auf dieselbe Klasse und verwenden Sie die ID wie zuvor wieder. Wenn es ausgeführt wird, werden die Beschriftungen nie angezeigt, wenn die Zellen in Controller 2 angezeigt werden. Funktioniert gut in Controller # 1.

Dies wird erwartet. Während beide Zellen dieselbe Klasse hatten, enthielt die Ansichtshierarchie, die in Controller 2 an die Zelle übergeben wurde, keinerlei Unteransichten. Sie haben also eine leere Zelle, genau das haben Sie in den Prototyp eingefügt.

Entwarf jeden Zelltyp in einer anderen NIB und verdrahtete ihn mit der entsprechenden Zellklasse. Fügen Sie im Storyboard eine leere Prototypzelle hinzu und legen Sie deren Klasse und Wiederverwendungs-ID fest, um auf meine Zellenklasse zu verweisen. Registrieren Sie in den viewDidLoad-Methoden der Controller diese NIB-Dateien für die Wiederverwendungs-ID. Wenn gezeigt, waren die Zellen in beiden Controllern wie der Prototyp leer.

Auch dies wird erwartet. Die Wiederverwendungskennung wird nicht zwischen Storyboard-Szenen oder Schreibfedern geteilt, sodass die Tatsache, dass alle diese unterschiedlichen Zellen dieselbe Wiederverwendungskennung hatten, bedeutungslos war. Die Zelle, die Sie aus der Tabellenansicht zurückerhalten, hat ein Erscheinungsbild, das der Prototypzelle in dieser Szene des Storyboards entspricht.

Diese Lösung war jedoch nah. Wie Sie bereits bemerkt haben, können Sie einfach programmgesteuert aufrufen -[UITableView registerNib:forCellReuseIdentifier:]und UINibdie Zelle übergeben, und Sie erhalten dieselbe Zelle zurück. (Dies liegt nicht daran, dass der Prototyp die Feder "überschrieben" hat. Sie hatten die Feder einfach nicht in der Tabellenansicht registriert, sodass immer noch die im Storyboard eingebettete Feder angezeigt wurde.) Leider gibt es einen Fehler bei diesem Ansatz - Es gibt keine Möglichkeit, Storyboard-Segmente an eine Zelle in einer eigenständigen Feder anzuschließen.

Bewahrte Prototypen in beiden Controllern leeren und setzen die Klasse und verwenden die ID wieder für meine Zellenklasse. Konstruierte die Benutzeroberfläche der Zellen vollständig in Code. Zellen funktionieren perfekt in allen Controllern.

Natürlich. Hoffentlich ist das nicht überraschend.


Deshalb hat es nicht funktioniert. Sie können Ihre Zellen in eigenständigen Schreibfedern entwerfen und in mehreren Storyboard-Szenen verwenden. Sie können derzeit keine Storyboard-Segmente mit diesen Zellen verbinden. Hoffentlich haben Sie beim Lesen etwas gelernt.

BJ Homer
quelle
Ah, ich verstehe. Sie haben mein Missverständnis verstanden - die Ansichtshierarchie ist völlig unabhängig von meiner Klasse. Rückblickend offensichtlich! Danke für die tolle Antwort.
Cliff W
Nicht mehr unmöglich, wie es scheint: stackoverflow.com/questions/8574188/…
Rich Apodaca
7
@RichApodaca Ich habe diese Lösung in meiner Antwort erwähnt. Aber es ist nicht im Storyboard; Es ist in einer separaten Feder. Sie konnten also keine Segues verkabeln oder andere Storyboard-ähnliche Dinge tun. Daher wird die ursprüngliche Frage nicht vollständig angesprochen.
BJ Homer
Ab XCode8 scheint die folgende Problemumgehung zu funktionieren, wenn Sie nur eine Storyboard-Lösung wünschen. Schritt 1) ​​Erstellen Sie Ihre Prototypzelle in einer Tabellenansicht in ViewController # 1 und ordnen Sie sie der benutzerdefinierten UITableViewCell-Klasse zu. Schritt 2) Kopieren Sie diese Zelle in die Tabellenansicht von ViewController # 2 und fügen Sie sie ein. Im Laufe der Zeit müssen Sie daran denken, Aktualisierungen manuell auf Kopien der Zelle zu übertragen, indem Sie Kopien löschen, die Sie im Storyboard erstellt haben, und den aktualisierten Prototyp wieder einfügen.
Jengelsma
Tolle Antwort, ich habe jedoch eine Folgefrage:> "Ein Storyboard ist im Wesentlichen nicht viel mehr als eine Sammlung von .xib-Dateien" Wenn dies der Fall ist, warum ist es so schwierig, eine xib einzubetten? ein Storyboard?
Willcwf
58

Trotz der großartigen Antwort von BJ Homer habe ich das Gefühl, eine Lösung zu haben. Soweit meine Tests gehen, funktioniert es.

Konzept: Erstellen Sie eine benutzerdefinierte Klasse für die xib-Zelle. Dort können Sie auf ein Touch-Ereignis warten und den Übergang programmgesteuert durchführen. Jetzt brauchen wir nur noch einen Verweis auf den Controller, der den Segue ausführt. Meine Lösung besteht darin, es einzustellen tableView:cellForRowAtIndexPath:.

Beispiel

Ich habe eine DetailedTaskCell.xibmit einer Tabellenzelle, die ich in mehreren Tabellenansichten verwenden möchte:

DetailedTaskCell.xib

TaskGuessTableCellFür diese Zelle gibt es eine benutzerdefinierte Klasse :

Geben Sie hier die Bildbeschreibung ein

Hier geschieht die Magie.

// 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

Ich habe mehrere Segues, aber alle haben den gleichen Namen : "FinishedTask". Wenn Sie hier flexibel sein müssen, schlage ich vor, eine weitere Eigenschaft hinzuzufügen.

Der ViewController sieht folgendermaßen aus:

// 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

Es gibt vielleicht elegantere Wege, um dasselbe zu erreichen, aber - es funktioniert! :) :)

Ericteubert
quelle
1
Danke, ich implementiere diesen Ansatz in meinem Projekt. Sie können diese Methode stattdessen überschreiben, damit Sie den Indexpfad nicht abrufen und die Zeile selbst auswählen müssen: - (void) setSelected: (BOOL) ausgewählt animiert: (BOOL) animiert {[super setSelected: ausgewählt animiert: animiert]; if (ausgewählt) [self.controller performSegueWithIdentifier: self.segue sender: self]; } Ich dachte, Super würde die Zelle auswählen, wenn [super touchEnded: touch withEvent: event] aufgerufen wird. Wissen Sie, wann es ausgewählt ist, wenn es nicht vorhanden ist?
Thejaz
9
Beachten Sie, dass Sie mit dieser Lösung jedes Mal, wenn eine Berührung in einer Zelle endet, den Übergang auslösen. Dies schließt ein, wenn Sie einfach durch die Zelle scrollen und nicht versuchen, sie auszuwählen. Möglicherweise haben Sie besseres Glück -setSelected:, wenn Sie die Zelle überschreiben und den Übergang nur beim Übergang von NOzu auslösen YES.
BJ Homer
Ich habe besseres Glück mit setSelected:, BJ. Vielen Dank. In der Tat ist es eine unelegante Lösung (es fühlt sich falsch an), aber gleichzeitig funktioniert es, also benutze ich es, bis dies behoben ist (oder sich etwas in Apples Gericht ändert).
Ben Kreeger
16

Ich habe danach gesucht und diese Antwort von Richard Venable gefunden. Für mich geht das.

iOS 5 enthält eine neue Methode für UITableView: registerNib: forCellReuseIdentifier:

Um es zu verwenden, legen Sie eine UITableViewCell in eine Schreibfeder. Es muss das einzige Stammobjekt in der Feder sein.

Sie können die Schreibfeder nach dem Laden Ihrer tableView registrieren. Wenn Sie dann dequeueReusableCellWithIdentifier aufrufen: Mit der Zellenkennung wird sie aus der Schreibfeder gezogen, genau wie wenn Sie eine Storyboard-Prototypzelle verwendet hätten.

Odrakir
quelle
10

BJ Homer hat eine ausgezeichnete Erklärung gegeben, was los ist.

Aus praktischer Sicht würde ich hinzufügen, dass, da Sie keine Zellen als xibs UND Verbindungssegmente haben können, die Zelle am besten als xib ausgewählt werden kann - Übergänge sind viel einfacher zu pflegen als Zellenlayouts und -eigenschaften an mehreren Stellen und Ihre Segmente unterscheiden sich wahrscheinlich sowieso von Ihren verschiedenen Controllern. Sie können den Übergang direkt von Ihrem Tabellenansichts-Controller zum nächsten Controller definieren und im Code ausführen. .

Ein weiterer Hinweis ist, dass Ihre Zelle als separate xib-Datei verhindert, dass Sie Aktionen usw. direkt mit dem Tabellenansichts-Controller verbinden können (das habe ich sowieso nicht geklärt - Sie können den Eigentümer der Datei nicht als sinnvoll definieren ). Ich arbeite daran, indem ich ein Protokoll definiere, dem der Tabellenansichts-Controller der Zelle entsprechen soll, und den Controller als schwache Eigenschaft, ähnlich einem Delegaten, in cellForRowAtIndexPath hinzufüge.

jrturton
quelle
10

Swift 3

BJ Homer gab eine ausgezeichnete Erklärung. Es hilft mir, das Konzept zu verstehen. An make a custom cell reusable in storyboard, die in jedem TableViewController verwendet werden können, müssen wir uns mix the Storyboard and xibnähern. Angenommen, wir haben eine Zelle mit dem Namen CustomCell, die im TableViewControllerOneund verwendet werden soll TableViewControllerTwo. Ich mache es in Schritten.
1. Datei> Neu> Klicken Sie auf Datei> Cocoa Touch-Klasse CustomCellauswählen > klicken Sie auf Weiter> Geben Sie beispielsweise den Namen Ihrer Klasse an > wählen Sie Unterklasse als UITableVieCell aus> Aktivieren Sie das Kontrollkästchen Auch XIB-Datei erstellen und klicken Sie auf Weiter.
2. Passen Sie die Zelle nach Ihren Wünschen an und legen Sie den Bezeichner im Attributinspektor für die Zelle fest. Hier legen wir fest, wie CellIdentifier. Diese Kennung wird in Ihrem ViewController verwendet, um die Zelle zu identifizieren und wiederzuverwenden.
3. Jetzt müssen wir nur nochregister this cellin unserem ViewControllerviewDidLoad. Keine Initialisierungsmethode erforderlich.
4. Jetzt können wir diese benutzerdefinierte Zelle in jeder Tabellenansicht verwenden.

In 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
quelle
5

Ich habe einen Weg gefunden, die Zelle für dieselbe VC zu laden, die nicht für die Segues getestet wurde. Dies könnte eine Problemumgehung zum Erstellen der Zelle in einer separaten Schreibfeder sein

Angenommen, Sie haben eine VC- und zwei Tabellen und möchten eine Zelle im Storyboard entwerfen und in beiden Tabellen verwenden.

(Beispiel: eine Tabelle und ein Suchfeld mit einem UISearchController mit einer Tabelle für Ergebnisse, und Sie möchten in beiden dieselbe Zelle verwenden.)

Wenn der Controller nach der Zelle fragt, gehen Sie wie folgt vor:

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

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

Und hier hast du deine Zelle aus dem Storyboard

João Nunes
quelle
Ich habe es versucht und es scheint zu funktionieren, JEDOCH werden die Zellen nie wiederverwendet. Das System erstellt und gibt jedes Mal neue Zellen frei.
GilroyKilroy
Dies ist die gleiche Empfehlung, die Sie unter Hinzufügen einer Suchleiste zu einer Tabellenansicht mit Storyboards finden . Wenn Sie interessiert sind, gibt es dort eine ausführlichere Erklärung dieser Lösung (Suche nach tableView:cellForRowAtIndexPath:).
Sinnvoll
aber das ist weniger Text und beantwortet die Frage
João Nunes