Besseres Key-Value-Coding

Ausgangslage

Im Buch wird erläutert, dass mutable Container als Attribute einer Entität eine gefährliche Sache sind. Allerdings kann man bei to-many-Relationships, welche durch NSArray oder NSSet repräsentiert werden, durchaus zu mutable Collections greifen, wenn man sich der Lage bewusst ist. Die Belohnung: Verbesserung des Laufzeitverhaltens.

1. Projekt anlegen

Legen Sie sich ein Projekt »Besseres KVC« als einfache Cocoa Application an.

2. Entität Person programmieren

Erzeugen Sie sich zunächst eine Klasse Person (*gähn schnnarch schon wieder augenroll*), die wie folgt aussieht:

Person.h
#import <Cocoa/Cocoa.h>

@interface Person : NSObject {
    NSString*   firstName;
    NSString*   lastName;
}
- (id)initWithFirstName:(NSString*)firstName 
            andLastName:(NSString*)lastName;

- (Person*)personWithFirstName:(NSString*)firstName 
                   andLastName:(NSString*)lastName;

- (NSString *)firstName;
- (void)setFirstName:(NSString *)value;

- (NSString *)lastName;
- (void)setLastName:(NSString*)value;
@end

Also eine Entität, die die beiden Attribute firstName und lastName hat. Dazu Accessoren. Schnell auch noch die Implementierung herunter getippt;

Person.m
#import "Person.h"

@implementation Person
- (NSString *)firstName {
    return [[firstName retain] autorelease];
}

- (void)setFirstName:(NSString *)value {
    if (firstName != value) {
        [firstName release];
        firstName = [value copy];
    }
}

- (NSString *)lastName {
    return [[lastName retain] autorelease];
}

- (void)setLastName:(NSString *)value {
    if (lastName != value) {
        [lastName release];
        lastName = [value copy];
    }
}


- (id)initWithFirstName:(NSString*)firstName_ 
            andLastName:(NSString*)lastName_ 
{

    self = [super init];
    
    if( self ) {
        [self setFirstName:firstName_];
        [self setLastName:lastName_];
    }
    
    return self;
}

+ (Person*)personWithFirstName:(NSString*)firstName_ 
                   andLastName:(NSString*)lastName_ 
{
    return [[[self alloc] initWithFirstName:firstName_ 
                                andLastName:lastName_] autorelease];
}

- (void) dealloc {
    [self setFirstName:nil];
    [self setLastName:nil];
    
    [super dealloc];
}
@end

Zu den hier verwendeten Namenskonventionen schauen sie bitte in das entsprechende Tutorial.

3. Entität Book programmieren

Da es ja um Beziehungen geht, müssen wir eine zweite Entität erzeugen, die in Bezug nimmt. Bauen wir uns eine Klasse Book (Sagen Sie nicht, dass Sie bereits damit gerechnet haben!!9!!8!!):

Book.h
#import <Cocoa/Cocoa.h>

@interface Book : NSObject {
    NSString*   title;
    NSArray*    authors;
}

- (NSString *)title;
- (void)setTitle:(NSString *)value;

- (NSArray *)authors;
- (void)setAuthors:(NSArray *)value;
@end

Also eine Entität mit einem Attribut title und einer to-many-Relationship authors. Die dazu gehörige Implementierung lautet dann;

Book.m
#import "Book.h"

@implementation Book
- (NSString *)title {
    return [[title retain] autorelease];
}

- (void)setTitle:(NSString *)value {
    if (title != value) {
        [title release];
        title = [value copy];
    }
}

- (NSArray *)authors {
    return [[authors retain] autorelease];
}

- (void)setAuthors:(NSArray *)value {
    if (authors != value) {
        [authors release];
        authors = [value retain];
    }
}

- (id)init {
    self = [super init];
    
    if( self ) {
        [self setTitle:@"Objective-C und Cocoa"];
        [self setAuthors:[NSArray array]];
    }
    
    return self;
}

- (void) dealloc {
    [self setTitle:nil];
    [self setAuthors:nil];
    [super dealloc];
}
@end

Auf eine Implementierung von umfangreicheren Initialisierern und convenience Allocators verzichten wir hier, um den Text kompakt zu halten. Wir werden diese Klasse nur im Interface Builder instantieren.

Mal eine Compilierung wagen, um die Teppfihler zu entdecken und zu beseitigen.

4. Interface Builder

Im Interface Builder lesen wir die Klasse Book und instantieren davon ein Objekt. Wir legen uns einen Object-Controller an, dessen Content-Outlet wir mit der Book-Instanz verbinden. Ziehen Sie zudem  einen Array-Controller herein, der unsere Autoren verwalten wird. Dessen contentArray -Binding binden Sie an den Object-Controller mit dem Model-Path authors .


Schwupp, Fenster aufgemacht und ein Textfield für den Titel angelegt und an den Object-Controller gebunden. Außerdem ein Tableview, dessen Spalten wir an den Array-Controller mit den Model-Paths firstName und lastName binden. Unten zwei Buttons für Einfügen und Löschen, welche Sie mittels ctrl-Drag mit den Actions add: und remove: des Array-Controllers verbinden. Alles speichern, kompilieren und starten. Das kennen Sie ja schon.


5. Manuelles Einfügen

Um besser sehen zu können, was passiert, schreiben wir uns eine eigene Einfügemethode. Wir benötigen also einen Controller, der eine Action - addAuthor: anbietet. Gehen Sie in den Interface Builder und erzeugen Sie mittels  Classes  |  Subclass NSObject  eine Subklasse von NSObject , die sie Controller nennen.

Sie können freilich diese Klasse auch wieder in Xcode erzeugen und in den Interface Builder importieren. Letztlich hängt es von Ihrem Geschmack ab. Ich versuche, beide Möglichkeiten gleich zu trainieren. Achten Sie bei der Erstellung von Klassen aus dem Interface Builder aber stets darauf, dass sie ihn aus dem Projekt gestartet haben. Nur so können die Dateien dem Projekt hinzugefügt werden.

Fügen Sie im Inspector der Klasse ein Outlet book und eine Action -addAuthor: hinzu und lassen Sie von dem Interface Builder mittels  Classes  |  Create Files for Controller  die Sourcen erzeugen. Zuletzt erstellen Sie eine Instanz im Nib. Ziehen Sie mit gedrückter ctrl-Taste die Verbindung von der Controller-Instanz zur Book-Instanz.

Bevor Sie jetzt den Interface Builder verlassen, fügen Sie einen weiteren Button ein, den Sie etwa mit »Add by Code« nennen. Diesen verbinden Sie mit der Action -addAuthor: des Controllers.

Speichern und zu Xcode wechseln. Der Header von Controller sollte jetzt so aussehen:

Controller.h
#import <Cocoa/Cocoa.h>

@interface Controller : NSObject
{
    IBOutlet Book *book;
}
- (IBAction)addAuthor:(id)sender;
@end

Wenn das Outlet mit id untypisiert sein sollte, so haben Sie vergessen, bei der Anlage im Interface Builder dem Outlet einen Typ zu geben. Das passiert leicht. Es ist nicht weiter schlimm. Dennoch sollten Sie in der Regel eine Typisierung vornehmen, wenn sie ein Objekt einer bestimmten Klasse erwarten. Also hinter die Ohren schreiben. Sie können das auch noch jetzt im Interface Builder ändern und dann die Dateien neu erzeugen lassen. Die zweimalige Nachfrage bestätigen Sie mit einem Klick auf Overwrite,

Nach dem Import fügen Sie bitte eine Zeile

Controller.h
@class Book;

ein, damit die Klasse Book bekannt ist. (Erde an Apple: Das könnte der Interface Builder auch automatisch machen, nicht wahr?)

Die Implementierung hat folgendes Aussehen:

Controller.m
#import "Controller.h"

@implementation Controller

- (IBAction)addAuthor:(id)sender {
}
@end

Zunächst setzen Sie unter das Bestehende Import zwei weiteres für unsere Klasse Book und Person :

Controller.m
#import "Book.h"
#import "Person.h"

Schreiben wir uns also wie im Buch beschrieben die Methode für das Einfügen:

Controller.m
- (IBAction)addAuthor:(id)sender
{
    //  Einen neuen Autor erzeugen
    Person* newPerson = [Person personWithFirstName:@"Amin" 
                                        andLastName:@"Negm-Awad"];
    
    //  Die bisherigen Autoren abholen und eine mutable Variante 
    //  aus dem Array machen
    NSMutableArray* authors 
        = [NSMutableArray arrayWithArray:[book authors]];
    
    //  Den neuen Autor einfuegen
    [authors addObject:newPerson];
    
    //  Und das Ganze wieder setzen
    [book setAuthors:authors];
}

Wenn Sie das Programm starten, so wird automatisch der Autor eingefügt.