Urlaub...

Jawohl, ich mache über Ostern Urlaub und mein Blog wahrscheinlich auch – es sei denn, es hat andere Pläne, oder das Wetter spielt nicht mit ;)
Allerdings schaffe ich es bei schlechtem Wetter ja vielleicht noch, Manfred zu retten…

KVC mal einfach - für Cocoa und Java

Die lästige Tipparbeit der Getter und Setter kann bei einigen Klassen in recht monotone, aber aufwändige Arbeit ausarten.
Gut, dass es kleine Helferlein gibt, die das Ganze automatisch erledigen.

Wer in Java entwickelt ist mit Eclipse gut beraten – über das Menü Source > Generate Getters and Setters können die Methoden automatisch generiert werden.
Entsprechende JavaDoc-Kommentare müssen jedoch noch „von Hand“ getippt werden.

Für Cocoa gibt es das kleine, aber mehr als nützliche Tool Accessorizer.
Die entsprechenden Variablendeklarationen werden in das linke Textfeld eingegeben oder kopiert.
Nun kann man sich aus diesen Variablen automatisch die Deklaration (inkl. Headerdoc-Kommentaren), Implementierung, Methoden für die Speicherung mit NSCoder, sowie die Deklaration von init und dealloc generieren lassen.
Bei init muss man nur noch die entsprechenden Default-Werte eintragen.
Accessorizer Screenshot

[more]

Sehr nützlich ist auch die Generierung von Indexed Accessors, wenn man z. B. mit einem NSMutableArray arbeitet.

Beispiele des generierten Codes:

Variablen:


NSString *_firstname;
NSString *_lastname;
NSMutableArray *_orders;

Das Präfix für Member-Variablen kann man unter Options einstellen. Hier ist ein Unterstrich eingestellt, aber es können auch andere Werte (z. B. ein leerer String für kein Präfix) verwendet werden.

Deklaration:


/*!
* @method firstname
* @abstract the getter corresponding to setFirstname
* @result returns value for firstname
*/
- (NSString *)firstname;
/*!
* @method setFirstname
* @abstract sets firstname to the param
* @discussion 
* @param newFirstname 
*/
- (void)setFirstname:(NSString *)newFirstname;
	

/*!
* @method lastname
* @abstract the getter corresponding to setLastname
* @result returns value for lastname
*/
- (NSString *)lastname;
/*!
* @method setLastname
* @abstract sets lastname to the param
* @discussion
* @param newLastname
*/
- (void)setLastname:(NSString *)newLastname;

/*!
* @method orders
* @abstract the getter corresponding to setOrders
* @result returns value for orders
*/
- (NSMutableArray *)orders;
/*!
* @method setOrders
* @abstract sets orders to the param
* @discussion
* @param newOrders
*/
- (void)setOrders:(NSMutableArray *)newOrders;

Das Präfix „new“ vor einer zu setzenden Variable ist ebenfalls in den Optionen einstellbar (Standardwert ist „a“).

Implementierung:


// init
- (id)init
{
    if (self = [super init]) {
        [self setFirstname: <(NSString *)newFirstname>];
        [self setLastname: <(NSString *)newLastname>];
        [self setOrders: <(NSMutableArray *)newOrders>];
    }
    return self;
}
	

//===========================================================
// firstname
//===========================================================
- (NSString *)firstname { return _firstname; }
- (void)setFirstname:(NSString *)newFirstname
{ if (_firstname != newFirstname) { [newFirstname retain]; [_firstname release]; _firstname = newFirstname; }
}

//===========================================================
// lastname
//===========================================================
- (NSString *)lastname { return _lastname; }
- (void)setLastname:(NSString *)newLastname
{ if (_lastname != newLastname) { [newLastname retain]; [_lastname release]; _lastname = newLastname; }
}

//===========================================================
// orders
//===========================================================
- (NSMutableArray *)orders { return _orders; }
- (void)setOrders:(NSMutableArray *)newOrders
{ if (_orders != newOrders) { [newOrders retain]; [_orders release]; _orders = newOrders; }
}

//===========================================================
// dealloc
//===========================================================
- (void)dealloc
{ [_firstname release]; [_lastname release]; [_orders release];

_firstname = nil; _lastname = nil; _orders = nil; [super dealloc]; }

Das Verhalten der Setter, sowie der dealloc-Methode ist über die Optionen einstellbar.

Indexed Accessors: (in diesem Fall nur orders)


///////  _orders  ///////
	

- (unsigned int)countOfOrders;
- (id)objectInOrdersAtIndex:(unsigned int)index;
- (void)insertObject:(id)anObject inOrdersAtIndex:(unsigned int)index;
- (void)removeObjectFromOrdersAtIndex:(unsigned int)index;
- (void)replaceObjectInOrdersAtIndex:(unsigned int)index withObject:(id)anObject;

/* Implementierung */
/////// _orders ///////

- (unsigned int)countOfOrders
{ return [[self orders] count];
}

- (id)objectInOrdersAtIndex:(unsigned int)index
{ return [[self orders] objectAtIndex:index];
}

- (void)insertObject:(id)anObject inOrdersAtIndex:(unsigned int)index
{ [[self orders] insertObject:anObject atIndex:index];
}

- (void)removeObjectFromOrdersAtIndex:(unsigned int)index
{ [[self orders] removeObjectAtIndex:index];
}

- (void)replaceObjectInOrdersAtIndex:(unsigned int)index withObject:(id)anObject
{ [[self orders] replaceObjectAtIndex:index withObject:anObject];
}

Das Programm kann die Implementierung auch in Java ausgeben, allerdings halte ich hier Eclipse für das bessere Programm.

Beide Programme können eine Menge (Tipp)Arbeit ersparen und dabei auch Fehler verringern.

Core Data versus Datenbank

Core-Data ist eine Art »XML-Datenbank« für Cocoa-Programme, die mit Tiger eingeführt wurde. Es können dabei SQL-ähnliche Abfragen mit NSPredicates und Relationen zwischen den verschiedenen »Objekten« erstellt werden.
Allerdings stellt sich sehr bald die Frage der Vor- und Nachteile gegenüber den herkömmlichen Methoden der Speicherung als Datei (meistens eine oder mehrere Plists), oder in einer Datenbank (PostgreSQL, MySQL …). Und vor allem: die Einträge sind alle in einer XML-Datei gespeichert — wie performant ist diese, wenn viele Daten verwaltet werden müssen?

Vorteile

  • Core Data geht Hand in Hand mit Cocoa-Bindings — man benötigt weniger GlueCode

  • Um die Speicherung und das Wiederherstellen der Daten braucht man sich keine Gedanken zu machen.

  • Daten und Relationen können in in XCode erstellt werden

  • Der Benutzer benötigt keine vorhandene Installation einer Datenbank

  • weniger Klassen und GlueCode, da man die meisten Entities ohne Bearbeitung übernehmen kann

Nachteile

  • Die mit Core Data erstellten Programme stehen nur Nutzern mit Tiger zur Verfügung — dieser wiegt für mich am schwersten

  • Speichermanagement: Die Entities werden beim Starten der Applikation in den Speicher geladen — Datenbankabfragen können dagegen jederzeit released werden.

  • Die Daten in einer SQL-Datenbank stehen auch Webapplikationen o. ä. zur Verfügung.

Generell hat Core Data ein schönes Konzept, welches die Arbeit des Entwicklers erleichtert. Allerdings sind Applikationen, die mit Core Data realisiert sind, nur unter Mac OS X 10.4 oder höher lauffähig, was einige Benutzer ausschliesst.
Über die Performance der einzelnen XML-Datei verglichen mit einer Relationalen Datenbank habe ich bisher leider keinerlei Dokumentation finden können.
Ich denke jedoch, dass sich der Performance-Unterschied erst ab einer grossen Menge von Daten bemerkbar macht.

Letztendlich bleibt die Frage der Datenspeicherung dem Entwickler überlassen — für welches Modell er sich entscheided, hängt dabei dann von den Anforderungen an das Programm ab.

NSArrayController aktuelle SortDescriptors speichern

Wer die Werte eines NSTableView mittels eines NSArrayControllers und Bindings füllt, braucht sich um die Implementierung der Funktionen zur Sortierung der Daten nach Tabellenspalten keine Gedanken zu machen. Gespeichert wird die aktuelle Sortierung jedoch nicht automatisch — nach einem Neustart der Applikation erscheinen die Werte wieder unsortiert.

Für den Benutzer ist es allerdings schöner, wenn nach einem Neustart die Daten in der Form sortiert sind, wie sie es beim Beenden des Programms waren.
Die Sortierung des Array innerhalb des NSArrayController geschieht über ein Array, welches eines oder mehrere Objekte der Klasse NSSortDescriptor enthält.
Der Startwert für die Sortierung der Werte im NSArrayController kann ebenfalls mittels Bindings gesetzt werden (unter dem Punkt sortDescriptors im Bindings-Inspektor des Controllers). Wir müssen also nur noch den entsprechenden Wert beim Beenden der Applikation (am besten in den NSUserDefaults) speichern und hier wieder einlesen.
Da man in den NSUserDefaults keine Objekte speichern kann, müssen die Informationen als NSData gespeichert werden.

- (void)applicationWillTerminate:(NSNotification *)notification
{
    NSData *data = [NSArchiver archivedDataWithRootObject:[arrayController sortDescriptors]];
    [[NSUserDefaults standardUserDefaults] setObject:data forKey:@"tableSortDescriptors"];
}

arrayController ist in diesem Fall ein IBOutlet, welches im IB mit dem NSArrayController verbunden ist.

Im InterfaceBuilder geben wir nun folgende Werte für den Punkt »sortDescriptors« des ArrayControllers an:

Bind to: Shared User Defaults
Controller Key: values
Model Key Path: tableSortDescriptors

Unter Value Transformer: wählen wir nun noch aus dem DropDown-Menü NSUnarchiveFromData, damit die Werte wieder »unarchiviert« werden.

Nun sollte bei einem Neustart die letzte Sortierung (inklusive der Selektion der Tabellenspalte) wieder hergestellt werden.

MySQL in Cocoa mit Objective-C nutzen

Da Objective-C auf C basiert, kann man die C-API für den Zugriff auf eine MySQL-Datenbank nutzen.
Damit dies funktioniert, müssen jedoch die entsprechenden Header im Cocoa-Projekt mit verlinkt werden. Ausserdem muss die libz.dylib eingebunden werden.

Damit unsere Applikation die entsprechenden Header-Dateien und Bibliotheken finden kann, müssen sie in den Build-Settings eingestellt werden.
Dazu das aktive Target auswählen und oben im Reiter auf Build klicken.
Folgende Werte müssen geändert werden

  • Header Search Paths: Der Pfad zum include-Verzeichnis der MySQL-Installation (in der Regel »/usr/local/mysql/include«)

  • Library SearchPaths: Der Pfad zum lib-Verzeichnis der MySQL-Installation (in der Regel »/usr/local/mysql/lib«). Ausserdem sollte der Pfad zur libz.dylib («/usr/lib/libz.dylib«») angegeben werden.

  • Other Linker Flags: -lmysqlclient

Die Build-Settings des Target

Da leider das Einbinden der libz.dylib über nicht immer reicht, sollte man die aktuelle Version zum Projekt hinzfügen. Um die aktuelle Version heraus zu finden im Terminal mit cd /usr/lib in das Verzeichnis wechseln und dort dann ein ls -la durchführen, um sehen zu können, auf welche Datei der Symlink libz.dylib zeigt.

Nun im Finder mittels Gehe Zu > Gehe zum Ordner… in das Verzeichnis /usr/lib wechseln und die Datei in unser Cocoa-Projekt wechseln.
Copy items into destination group’s folder (if needed) wählen und bestätigen.

Nun muss nur noch in den Klassen, in denen auf die Datenbank zugegriffen werden soll, die Header-Datei »mysql.h« importiert werden.

Anmerkung: der Build-Style Development verursacht oft Probleme, oder Fehlermeldungen. Diese treten jedoch im Deployment nicht auf. Daher wechsle ich meist direkt zu Deployment, da ich nur dort sehen kann, ob »richtige« Fehler vorhanden sind.

Bugfix in All My Documents 1.2

Deutsch

Aus aktuellem Anlass mal wieder ein mehrsprachiger Artikel ;)
Ich war dieses Mal ein wenig zu schnell, was das Veröffentlichen des neuen Releases angeht. Das Problem lag darin, dass es bei diesem Release einfacher war, (fast) komplett von vorne anzufangen, statt den alten Code zu verwenden, da das Projekt nun mit Bindings realisiert wurde.
Leider habe ich eine kleine, aber wichtige Funktion übersehen und somit erst jetzt eingebaut: das Verhalten des Fensters, wenn es erneut geöffnet wird.
Ich habe heute Nachmittag ein Update der Version auf Sourceforge gemacht.
Wer sich also die neue Version schon vorher geladen hat, sollte bitte ein Update machen und sich die 1.2 noch einmal laden.

English


Today I updated the version at sourceforge because of a little bug with the window’s reappereance after closing.
If you downloaded the 1.2 before, just reload it and install it over the existing one.

Cocoa: Intelligente Ordner mit NSPredicate

In der Regel hat man eine Liste von Objekten, deren Inhalt man darstellen möchte, in einem NSArray oder NSMutableArray gespeichert und greift über die Indizes auf die Inhalte zu, um sie in einer Tabelle oder ähnlichem Darzustellen.

[more]

Beispiel:

Man hat eine Klasse Adresse, in der Name, Vorname und Telefonnummer gespeichert werden (ich gebe zu, ich bin sehr kreativ, was Beispiele angeht ;) ).
In dieser Klasse werden nun nur die entsprechenden Member-Variablen angelegt und die dazugehörigen Getter- und Setter-Methoden.

@interface Adresse : NSObject {
NSString *_name;
NSString *_vorname;
NSString *_telefon; //Wir nehmen einen String, da auch Sonderzeichen enthalten sein können
}

	

- (NSString *)name; - (void)setName:(NSString *)newName;

- (NSString *)vorname; - (void)setVorname:(NSString *)newVorname;

- (NSString *)telefon; - (void)setTelefon:(NSString *)newTelefon;

@end

Die Implementierung beinhaltet nur die entsprechenden Getter- und Setter-Methoden, daher werde ich sie hier nicht einfügen.

Um die Adressen zu verwalten, legen wir einen Controller an, der über die üblichen Accessoren für ein Array verfügt:

@interface AdressController : NSObject {
NSMutableArray *_adressen;
}

	

- (NSMutableArray *)adressen; - (void)setAdressen:(NSMutableArray *)newAdressen;

- (unsigned int)countOfAdressen; - (id)objectInAdressenAtIndex:(unsigned int)index; - (void)insertObject:(id)anObject inAdressenAtIndex:(unsigned int)index; - (void)removeObjectFromAdressenAtIndex:(unsigned int)index; - (void)replaceObjectInAdressenAtIndex:(unsigned int)index withObject:(id)anObject;

@end

Die Implementierung bleibt wieder einmal bei euch ;)

Um nun Intelligente Ordner für die Adressen anlegen zu können (z. B. alle Adressen, deren Name mit „Be“ beginnt, benötigen wir erst einmal eine Klasse für die Ordner:

@interface smartFolder : NSObject {
NSPredicate *_predicate; //Unser Predicate zur Filterung des Arrays
}

	

- (NSPredicate *)predicate; - (void)setPredicate:(NSPredicate *)newPredicate;

- (NSArray *)adressen; //Hier werden die gefilterten Adressen ausgegeben @end

Um auf die Adressen zugreifen zu können, benötigen wir die jeweils aktuellen Adressen des AdressController. Also fügen wir in diesem eine statische Variable sharedAdressController hinzu, sowie eine Methode _+ (AdressController *)sharedAdressController_

//nach @implementation fügen wir eine statische Variable ein und weisen ihr den Wert nil zu
static sharedAdressController *sharedController = nil;

//in der Init-Methode weisen wir sharedAdressController uns selbst zu - (id)init {

if (self = [super init]) { //hier erfolgen Zuweisungen } sharedAressController = self; return self; }

//Implementierung der Methode für unser Singleton + (AdressController *)sharedAdressController { @synchronized(self) { if (sharedAdressController == nil) { sharedAdressController = [[self alloc] init]; } } return sharedAdressController; }

Nun können wir nach einem Import der Klasse in unserem „SmartFolder“ auf die Instanz zugreifen und so die aktuellen Adressen bekommen.

-(NSArray *)adressen
{
NSArray *gefiltert = [[[AdressController sharedAdressController] adressen] filteredArrayUsingPredicate:[self predicate]];
return gefiltert;
}

Durch die Methode filteredArrayUsingPredicate: kann man ein Array mittels eines Ausdrucks (ähnlich SQL) filtern und so ein Array mit den entsprechenden Werten erhalten.
Wollen wir einen Intelligenten Ordner erstellen, der alle Adressen enthält, deren Name mit „Be“ beginnt, so könnten wir den Ausdruck SELF.name beginswith[c] 'be' verwenden.
Es können aber auch genauso Reguläre Ausdrücke oder die Operatoren < > || && usw verwendet werden.

Mehr Informationen über NSPredicates erfahrt ihr im Predicates Programming Guide bei Apple Developer

Cocoa: NSBox mit Hintergrundfarbe und 3D-Rahmen

Im Beispielcode habe ich eine Subklasse von NSBox erstellt, es funktioniert aber genauso mit jeder anderen Unterklasse von NSView, oder NSView selbst.

Sowohl NSView, als auch seine Unterklasse NSBox bieten leider im Interface-Builder keine Möglichkeit, eine Hintergrundfarbe festzulegen, oder einen rechteckigen 3D-Rahmen zu erzeugen.

[more]

Um diese Effekte zu erreichen, muss man eine Unterklasse von NSView (oder NSBox) erstellen und die Methode

- (void) drawRect: (NSRect)rect
{
}

überschreiben, die dazu dient, das NSView zu »zeichnen«.

Für unser NSView benötigen wir zwei NSRect — eines für den Rahmen und darin ein etwas kleineres, mit dessen Hilfe wir den Hintergrund zeichnen können.

 – (void) drawRect: (NSRect)rect
{ NSRect backgroundRect, borderRect;

//Unser Rahmen-NSRect soll die Maße des Views bekommen borderRect = [self bounds]; //das NSRect für den Hintergrund soll 2 Pixel kleiner sein backgroundRect = NSInsetRect(borderRect, 2.0, 2.0); }
Da wir zwar einen einfarbigen Rahmen um ein NSRectzeichnen können, jedoch leider keinen mehrfarbigen, müssen wir diesen aus mehreren Linien selbst »bauen«. Hierfür benötigen wir die vier Eckpunkte, die wir mittels der NSMin- und NSMax-Funktionen ermitteln können. Die Funktionen erwarten jeweils einen Parameter vom Typ NSRect.
 – (void) drawRect: (NSRect)rect
{
    …
    int minX = NSMinX(borderRect); //kleinster x-Wert (die linke Seite)
    int maxX = NSMaxX(borderRect); //gößter x-Wert (die rechte Seite)
    int minY = NSMinY(borderRect); //kleinster y-Wert (unten)
    int maxY = NSMaxY(borderRect); //größter y-Wert (oben)

    //Nun benötigen wir zwei NSBezierPath — einen für den linken/oberen Rand und einen für den rechten/unteren
    NSBezierPath *grayPath  = [NSBezierPath bezierPath];
    NSBezierPath *whitePath = [NSBezierPath bezierPath];

    //Die Rahmen links und oben sollen in grau gezeichnet werden
    [[NSColor grayColor] set];
    //Damit man den Rahmen auch gut sieht, setzen wir eine Strichstärke von 2.0
    [grayPath setLineWidth:2.0];

    //Wir beginnen unseren Pfad in der Ecke links unten und zeichnen nach links oben
    [grayPath moveToPoint: NSMakePoint(minX, minY)];
    //Linie nach links oben
    [grayPath lineToPoint: NSMakePoint(minX, maxY)];
    //von hier aus nach rechts oben
    [grayPath lineToPoint: NSMakePoint(maxX, maxY)];
    //Unser erster Pfad ist fertig, also zeichnen wir ihn mit der eingestellten Farbe
    [grayPath stroke];

    //Für die anderen beiden Seiten nehmen wir eine weiße Rahmenfarbe
    [[NSColor whiteColor] set];
    [whitePath setLineWidth:2.0];
    //wir beginnen oben rechts und zeichnen die beiden Rahmenlinien
    [whitePath moveToPoint: NSMakePoint(maxX, maxY)];
    [whitePath lineToPoint: NSMakePoint(maxX, minY)];
    [whitePath lineToPoint: NSMakePoint(minX, minY)];
    [whitePath stroke];
}
Damit haben wir schon einmal unseren äußeren Rahmen. Nun müssen wir den Hintergrund zeichnen. Hierzu erstellen wir einen NSBezierPath aus unserem backgroundRect und füllen es mit weiß. Da wir die weiße Farbe für unsere letzten beiden Linien benutzt haben, ist sie noch gesetzt. Wer eine andere Hintergrundfarbe benutzen möchte, muss sie vorher erst noch setzen.
- (void) drawRect:(NSRect)rect
{
    ...
    [[NSBezierPath bezierPathWithRect:backgroundRect] fill];
}

Nun haben wir jedoch das Problem, dass der weiße Rahmen rechts und unten mit dem Hintergrund verschmilzt. Um den Rahmen abzusetzen, ziehen wir noch eine dünne (1.0 Punkt) Linie um unseren Hintergrund. Da wir weiter oben bereits einmal grau als Zeichenfarbe verwendet haben, schreiben wir den Befehl einfach dort hinzu.

- (void) drawRect: (NSRect)rect
{
    ...
    [[NSColor grayColor] set];
    //Hier ziehen wir den Rahmen
    [[NSBezierPath bezierPathWithRect:backgroundRect] stroke];
    ...
}

Wer statt dem eingesunkenen Rahmen lieber einen hervorgehobenen haben möchte, vertauscht einfach die Rahmenfarben.
Einen stärkeren Effekt erzielt man, wenn man den oberen und linken Rahmen in schwarz statt grau zeichnet.

Cocoa: NSTextView-Inhalte drucken


Bei nicht dokument-basierten Applikationen hat man immer das Problem, dass beim Druck der Inhalt vertikal auf der Seite zentriert wird.
Die Lösung ist eine eigene Print-Action in der Controller-Klasse.


-(IBAction)printContents:(id)sender {
NSPrintInfo *pi;
NSPrintInfo *sharedInfo;
NSPrintOperation *printOp;
NSMutableDictionary *piDict;
NSMutableDictionary *sharedDict;

sharedInfo = [NSPrintInfo sharedPrintInfo];
sharedDict = [sharedInfo dictionary];
piDict = [NSMutableDictionary dictionaryWithDictionary:
sharedDict];

pi = [[NSPrintInfo alloc] initWithDictionary: piDict];

/* Hier wird der Inhalt automatisch in Seiten aufgeteilt */
[pi setHorizontalPagination: NSAutoPagination];
[pi setVerticalPagination: NSAutoPagination];
[pi setVerticallyCentered:NO]; //Nicht mehr zentrieren /* Abstaende setzen */
[pi setTopMargin:40.0];
[pi setLeftMargin:10.0]; /* unserem 'Druckobjekt' sagen, dass es den Inhalt des TextViews drucken soll */
printOp = [NSPrintOperation intOperationWithView:textView printInfo:pi]; /* Wir moechten den Druckdialog angezeigt bekommen (Vorschau, etc) */
[printOp setShowPanels:YES]; /* Alle Einstellungen gemacht - also starten */
[printOp runOperation];
}

„textView“ ist der Name des Outlets für das NSTextView.
Nun muss man nur noch den entsprechenden Menüpunkt „Drucken“ mit der eigenen Aktion verbinden.

Das piDict habe ich nur mit hereingenommen, damit ich die Möglichkeit habe, die Funktion z. B. um eine PDF-Speicherung zu erweitern.

Die Margins kann man natürlich noch verändern ­ je nach Belieben. Wer möchte, kann sie auch aus einer plist beziehen.