Tutorial: Storyboard und Delegate

Hallo Zusammen,

heute schreibe ich über Delegate. Die Übersetzung legt schon nahe worum es geht – beauftragen, übertragen. Mit Delegates überträgt man Aufgaben auf eine Instanz einer anderen Klasse – dem Delegate-Empfänger. Welche Aufgaben das sind wird im Delegate definiert. Man könnte auch sagen über Delegates wird der Delegate-Empfänger über Ereignisse eines Objektes informiert.

In Objektive-C ist ein Delegate vom Typ NSObject. Auf diese Weise ist sicher gestellt, dass jede Instanz einer beliebigen anderen Klasse dessen Delegate werden kann. Damit eine unsere eigenen Klassen zum Delegate-Empfänger werden kann müssen wir alle Pflichtmethoden des Delegates implementieren.

In diesem Tutorial werden wir eine Anwendung programmieren die eine Tabelle darstellt. Über einen Button gelangt man zu einer zweiten View in welcher neue Einträge angelegt werden können. Diese zweite View wird nach dem erfolgreichen Erstellen eines Eintrages die Tabelle darüber informieren, dass ein neuer Eintrag vorliegt.

Bevor wir jedoch das erste Delegate betrachten, müssen wir uns ein wenig mit Storyboards beschäftigen.

Wir erstellen uns ein neues Projekt vom Typ „Single View Application“. Das Template liefert uns gleich eine ViewController Klasse mit, die wir nicht benötigen. Wir löschen also ViewController.h und ViewController.m. Nun erstellen wir über New » File einmal einen UITableViewController mit dem Namen „TableViewController“ und einen UIViewController mit dem Namen „InputView Controller“.

Wechseln wir ins iPhone Storyboard begegnet uns ein einsamer ViewController. Der reicht nicht für unsere Zwecke und wir ziehen einen TableViewController ins Storyboard. Diesen TableViewController betten wir nun in einen NavigationController ein (Editor » Embed In » Navigation Controller). Über den Identity Inspector ändern wir die Klassen unserer ViewController im Storyboard.

UITableViewController » TableViewController und UIViewController » InputViewController.

Delegate Identifier

Zu guter Letzt treffen wir noch eine Vorbereitung für später: Wir sollten den Zellen der Tabelle einen Reuse Identifier geben – das ist ein Name mit welchem wir Xcode zu gegebener Zeit sagen können „baue mir eine Zelle, so wie die im Storyboard“. Wir wählen also die Zelle an wechseln zum Attribute Inspector und setzten identifier auf „DataCell“, sowie den Style auf „Basic“.

Delegate Cell Identifier Anschließend benötigen wir noch ein paar Steuerelemente. In den TableViewController ziehen wir ein BarButtonItem und setzen über den Attribut Inspector dessen Identifier auf „Add“. Das ändert zwar nur das Aussehen und der Button funktioniert mit „Item“ genauso, aber wir sollten es gleich ordentlich machen. Wir möchten das der Nutzer beim touch auf + zum InputViewController gelangt um dort einen neuen Eintrag für unsere Liste anlegen zu können. Mit markierten + Button und gedrückter ctrl-Taste ziehen wir nun eine Verbindung zum InputViewController. Im anschließend erscheinenden Auswahlmenü nehmen wir eine push segue. Die segue kann man an dem Kreis in der Mitte anwählen. Wir sollten ihr einen Namen verpassen und gehen dazu in den Attribute Inspector und setzen identifier auf „inputSegue“.

Startet die App – bei einem touch auf das + gelangt ihr zum InputViewController. Leider ist der noch leer und sieht daher ziemlich unspektakulär aus. Darum kümmern wir uns jetzt.

Dem InputViewController spendieren wir einen Save und Cancel BarButtonItem. Außerdem bekommt er eine schöne Überschrift und ein UITextField, welches wir ein wenig vergrößern.

delegate bar button item

Steigen wir nun in den Code des TableViewController ein. Eine Tabelle benötigt ein zugrunde liegendes Datenmodell welches sie anzeigen soll. Im TableViewController.h ergänzen wir daher ein Array als property.

@property (nonatomic, strong) NSMutableArray        *dataArray;

Im TableViewController.m müssen wir nun definieren wie viele  Sektionen und Zeilen die Tabelle haben soll. Für unsere Zwecke reicht eine Sektion und die Anzahl der Zeilen ist gleich der Anzahl der Elemente im Array.

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return [self.dataArray count];
}

Es fehlt noch die Definition des Inhalt der Zellen. Wir wollen das der Text der Elemente aus dem Array in je einer Zeile erscheint.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"DataCell"];
	NSString *string = [self.dataArray objectAtIndex:indexPath.row];
	cell.textLabel.text = string;
    return cell;
}

Hier benutzen wir unseren zuvor definierten Reuse Identifier der Zelle. Anschließend lesen wir den Text aus unserem Array und weisen ihm der Zelle zu. Allerdings ist unser Array noch leer und nicht einmal instanziiert. Wir sollten uns im AppDelegate ein Array anlegen und an die Instanz des TableViewController übergeben.

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    NSMutableArray *dataArray = [NSMutableArray arrayWithObjects: [NSString stringWithFormat:@"incredible"],[NSString stringWithFormat:@"awesome"],[NSString stringWithFormat:@"iOSCampus"],[NSString stringWithFormat:@"tutorial"], nil];
    UINavigationController *navigationController =  (UINavigationController*)self.window.rootViewController;
    TableViewController *tableViewController = [[navigationController viewControllers] objectAtIndex:0];
    tableViewController.dataArray = dataArray;

    return YES;
}

Nachdem wir das Array mit Begriffen haben, müssen wir es an das dataArray property des TableViewController übergeben. Da wir keine unmittelbare Referenz haben müssen wir uns zum TableViewController hangeln. Zunächst wissen wir, dass unsere App mit einem Navigation Controller als rootViewController startet. Das erste Element das im Navigation Controller eingebettet ist, muss unser TableViewController sein, dem wir anschließend unser Array übergeben können. Bei einem Appstart sehen wir nun unsereTabelle mit Text gefüllt.

delegate simulator

Wir benötigen nun IBOutlets und IBActions. Das bedeutet wir möchten auf die Elemente, die wir im Storyboard definiert haben in unserem Quellcode zugreifen. Wechseln wir ins Storyboard und wählen den InputViewController an. Eine Möglichkeit IBOutlets und ABActions anzulegen ist mit Hilfe des Assistant Editors (View -> Assistant Editor oder das kleine Symbol oben recht mit dem Anzug und der Schleife). Wir können nun unsere BarButtons markieren und mit gedrückter ctrl-Taste eine Verbindung zum InputViewController.h ziehen. Wir erstellen auf diese Weise eine IBAction für die beiden BarButtonItems und ein IBOutlet für das Textfeld. Am Ende sieht der Header so aus:

@property (strong, nonatomic) IBOutlet UITextField *textField;
- (IBAction)save:(id)sender;
- (IBAction)cancel:(id)sender;

delegate IBOutlet

Kommen wir zu unserem ersten Delegate, dem UITextFieldDelegate. Wir ergänzen den Header:

@interface InputViewController : UIViewController <UITextFieldDelegate>

Ein Click mit cmd auf UITextFieldDelegate führt uns zu dessen Definition. Dort können wir erkennen, dass alle Methoden nur optional sind – wir müssen sie also nicht implementieren. Wir möchten jedoch darüber unterrichtet werden, wenn der Nutzer die Enter Taste drückt. In diesem Fall soll der Eintrag ebenso gespeichert werden, als hätte er save betätigt. In InputViewController.m ergänzen wir zunächst ViewDidAppear

- (void)viewDidAppear:(BOOL)animated{
    [super viewDidAppear:animated];

    [self.textField becomeFirstResponder];
    self.textField.delegate = self;
}

Über becomeFirstResponder wird das Textfeld bei erscheinen der View angewählt und die Tastatur fährt hoch – nicht notwendig aber für den Nutzer angenehm. Anschließend definieren wir, dass InputViewController der Delegate-Empfänger des Textfeldes sein soll. Nun können wir die ausgewählte Methode des UITextFieldDelegate implementieren:

- (BOOL)textFieldShouldReturn:(UITextField *)textField
{
    [self save:textField];
    return YES;
}

Wir rufen einfach nur die bereits vorliegende save Methode auf (unsere IBAction des save buttons). Save ist allerdings noch leer und es gibt auch noch keine Möglichkeit der TableViewController über unser fertig angelegtes Object zu informieren – wir sollten uns unser eigenes Delegate schreiben. Im InputViewController.h ergänzen wir oberhalb des @interface

@protocol InputViewControllerDelegate 
- (void)inputViewControllerDelegateDidCancel:(InputViewController *)controller;
- (void)inputViewControllerDelegate:(InputViewController *)controller didAddString:(NSString*)string;
@end

und zusätzlich unterhalb des bereits bestehenden propertys:

@property (nonatomic, weak)     id  inputDelegate;

Unser Delegate hat zwei nicht optinale Methode. Der Delegate Empfänger soll informiert werden, wenn der Nutzer auf cancel bzw aus save drückt. Das property dient der Zuweisung des Delegate-Empfängers. Wir können nun unsere IBActions vervollständigen.

- (IBAction)save:(id)sender{
   if (self.inputDelegate) {
        [self.inputDelegate inputViewControllerDelegate:self didAddString:self.textField.text];
    }   
}
- (IBAction)cancel:(id)sender{
    if (self.inputDelegate) {
        [self.inputDelegate inputViewControllerDelegateDidCancel:self];
    }
}

Jetzt muss der TableViewController noch angeben, dass er konform zum InputViewControllerDelegate Protokoll ist und die beiden Delegate Methoden implementieren.

#import "InputViewController.h"
@interface TableViewController : UITableViewController 

// in TableViewController.m

- (void)inputViewControllerDelegateDidCancel:(InputViewController *)controller{
    [self.navigationController popViewControllerAnimated:YES];
}

- (void)inputViewControllerDelegate:(InputViewController *)controller didAddString:(NSString *)string{
    [self.dataArray addObject:string];
    NSIndexPath *indexPath = [NSIndexPath indexPathForRow:[self.dataArray count] - 1 inSection:0];
    [self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
    [self.navigationController popViewControllerAnimated:YES];
}

Sobald nun also in der InputView auf save oder enter gedrückt wird, erhält der Delegate-Empfänger den Methodenaufruf inputViewControllerDelegate:didAddString. Als ersten fügen wir das neue Objekt in unsere Datenbasis, dem dataArray, ein. Mit einem NSIndexPath bestimmen wir die Stelle an dem der neue Eintrag eingefügt werden soll. Anschließend entfernen wir noch mit popViewControllerAnimated den InputViewController.

Als wirklich letzten Schritt müssen wir der Instanz des InputViewControllers noch mitteilen, wer ihr Delegate-Empfänger sein soll. Hierzu nutzen wir die Methode prepareForSegue, die immer aufgerufen wird bevor eine Segue statt findet. Wir nutzen hier den oben vergebenen identifier „inputSegue“, um zu identifizieren welche segue aufgerufen wird (da wir nur eine segue haben müssten wir das nicht tun, man sollte sich aber gleich daran gewöhnen). Wir wissen, dass das Ziel der Segue unser InputViewController ist. Mit der so erstellten Refenrenz können wir nun das Delegate setzen.

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{
    if ([segue.identifier isEqualToString:@"inputSegue"]){
        InputViewController *inputViewController = segue.destinationViewController;
        inputViewController.inputDelegate = self;
    }
}

 delegate input

Fertig :-)

11 thoughts on “Tutorial: Storyboard und Delegate

    • Habe versucht es ähnlich in mein Projekt zu übernehmen. Ich war der Ansicht, dass ich etwas falsch gemacht habe und habe alles wieder ausgelöscht bzw. auszementiert, doch nun funktioniert mein Programm / meine App nicht mehr mit der Fehlermeldung:

      Terminating app due to uncaught exception ‚NSUnknownKeyException‘, reason: ‚[ setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key Notizen.‘

      Was ich leider nicht ganz verstehe.

  • Höchstwahrscheinlich hast du früher mal im Storyboard Outlets gesetzt und diese in der Klasse beim überarbeiten gelöscht. Die Verbindung im Storyboard wird jedoch nicht automatisch mit gelöscht. Deshalb passt beides nicht mehr zusammen.

    Schau mal im Storyboard in der betreffenden Klasse. Dort dann im „Connections Inspector“ (rechte Spalte, rechter Tab). Dort müssten einige Einträge mit einem leeren Punkt gekennzeichnet sein – die sind zu löschen.

  • Ich habe das Programm komplett übernommen und kann es auch fehlerlos starten, jedoch sind die Button „cancel“ und „save“ anscheint ohne Funktion. Hat wer eine Idee wo ich schauen soll?

    • Hallo Max,

      ich vermute, dass deine Cancel und Save Buttons nicht mit dem Quellcode verbunden sind. Füge doch ein NSLog(@“cancel test“); in – (IBAction)cancel:(id)sender ein.
      Wenn du nun cancel drückst und in der Console nicht cancel test ausgegeben wird – dann fehlt die Verbindung.
      Die kannst du einfach herstellen, indem du den Button mit gedrückter ctrl Taste auf die jeweilige Methode ziehst. So wie du es bei + gemacht hast ;-)

  • Ich poste hier weiter um es übersichtlich zu halten.

    Du hast das inputDelegate nicht gesetzt. Damit hast du niemanden der die Nachricht empfangen soll.
    In prepareForSegue lege ich mit inputViewController.inputDelegate = self fest, dass der TableViewController der Delegate des InputViewControllers ist. Er soll die Nachrichten erhalten.

Schreibe einen Kommentar zu max Antworten abbrechen

Deine E-Mail-Adresse wird nicht veröffentlicht.