21.07.2012
Xcode 4, Cocoa, Cocoa Touch
Cocoa-Tutorial zum programmatischen Erstellen einer NSView/UIView
In dem folgenden Tutorial wird erklärt, wie man programmatisch eine NSView
mit Elementen befüllt und diese mit IBOutlets
und IBActions
verbindet. In dem Beispiel wird also nicht der InterfaceBuilder verwendet, um die View zu erstellen, sondern es wird mit Objective-C Anweisungen eine View mit NSTextField
-, NSButton
- und NSBox
-Elementen erstellt und mit IBOutlets und IBActions verbunden. Zusätzlich wird beschrieben, wie die NSTextField
-Objekte mit nexKeyView
so verbunden werden, dass mittels der Tab-Taste von einem Feld zum nächsten gesprungen werden kann.
Dieses Beispiel bezieht sich zwar auf Cocoa-Programmierung, ist aber problemlos auf die iOS-Entwicklung übertragbar. Das Vorgehen bei der programmatischen Erstellung einer UIView
ist prinzipiell genau gleich.
Der programmatische Ansatz kann bei sehr vielen Objekten in einer View Sinn machen. In dem folgenden Beispiel soll eine NSView
für ein Sudoku-Spielfeld erstellt werden. Das Spielfeld besteht aus 81 einzelnen Feldern, die teilweise von Rahmen und Linien voneinander abgesetzt werden. Die folgende Abbildung zeigt das fertige Spielfeld.
Wie man sich sehr leicht vorstellen kann, ist es sehr aufwändig eine solche Oberfläche mit dem InterfaceBuilder zu erstellen. Es muss jedes einzelne NSTextField erstellt, angeordnet und später mit einem IBOutlet verbunden werden, so dass mit dem Controller darauf zugegriffen werden kann. Die Definition der IBOutlet sähe ungefähr so aus:
IBOutlet NSTextField *F00; IBOutlet NSTextField *F01; IBOutlet NSTextField *F02; IBOutlet NSTextField *F03; IBOutlet NSTextField *F04; IBOutlet NSTextField *F05; IBOutlet NSTextField *F06; IBOutlet NSTextField *F07; IBOutlet NSTextField *F08; IBOutlet NSTextField *F10; IBOutlet NSTextField *F11; IBOutlet NSTextField *F12; ... IBOutlet NSTextField *F86; IBOutlet NSTextField *F87; IBOutlet NSTextField *F88;
Da eine solche Implementierung nicht besonders schön ist, soll im Folgenden eine Oberfläche programmatisch erstellt werden.
Die Idee beim Erstellen einer Oberfläche im Quellcode
Das Prinzip, welches hinter dem Erstellen einer View direkt im Programmcode steht ist, dass alle Komponenten bei Cocoa/Cocoa-Touch Views sind. Diese Views lassen sich problemlos ineinander schachteln. Deshalb ist es möglich in einer mainView mehrere SubViews mit der Methode addSubview:(NSView*)
hinzuzufügen.
Soll beispielsweise ein Button, ein TextField oder eine andere Komponente zu einer View hinzugefügt werden, muss nur sichergestellt sein, dass diese von NSView
oder UIView
abgeleitet ist. Dann ist ein Hinzufügen problemlos möglich.
Projekt anlegen
Zuerst wird ein neues Xcode-Projekt mit einer Cocoa-Application angelegt. Wie das gemacht wird, ist im Tutorial: Wie erstelle ich eine Custom View beschrieben.
Damit man Zugriff auf das NSWindow
und die NSView
aus dem xib
-Datei hat, muß für beide ein IBOutlet
in der Header-Datei AppDelegate.h angelegt werden. Dabei definieren wir noch ein NSMutableArray
in dem wir später die NSTextFields
ablegen.
// // AppDelegate.h // ProgrammaticallyAddToView // // Created by Jörn Hameister on 21.07.12. // Copyright 2012 http://www.hameister.org . All rights reserved. // #import <Cocoa/Cocoa.h> @interface AppDelegate : NSObject <NSApplicationDelegate> { @private NSWindow *window; NSMutableArray *textFields; IBOutlet NSView *mainView; } -(IBAction) doClickSolve:(id)sender; @property (assign) IBOutlet NSWindow *window; @property (assign) IBOutlet NSView *mainView; @end
In der xib-Datei muss dann noch per Drag&Drop eine Verbindung zwischen den beiden Outlets hergestellt werden. Dazu wechselt man zu der xib-Datei MainMenu.xib und stellt die Verbindung her.
In der Datei AppDelegate.m wird die fehlende synthesize
-Anweisung ergänzt, so dass die Datei folgendermaßen aussieht:
// // AppDelegate.m // SudokuField // // Created by Jörn Hameister on 21.07.12. // Copyright (c) 2012 http://www.hameister.org. All rights reserved. // #import "AppDelegate.h" @implementation AppDelegate @synthesize window = _window; @synthesize mainView = _mainView; - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { // Insert code here to initialize your application } @end
NSTextField
als SubView
Nun kann die Methode applicationDidFinishLaunching:(NSNotification *)
mit Leben gefüllt werden:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification { [window setContentSize:NSMakeSize(640, 680)]; NSRect viewBounds = [mainView bounds]; int offsetX = 0; int offsetY = 0; textFields = [[NSMutableArray alloc] init]; NSMutableArray *lines = [[NSMutableArray alloc]init]; for(int j=0;j<9;j++) { for(int i=0; i<9;i++) { NSTextField *textField = [[NSTextField alloc]init]; [textField setStringValue:@""]; [textField setFrameSize:NSMakeSize(50, 50)]; [textField setBordered:FALSE]; [textField setFont:[NSFont fontWithName:@"Verdana" size:30]]; [textField setAlignment:NSCenterTextAlignment]; [mainView addSubview:textField]; CGRect frame = textField.frame; if((j)%3==0 && i==0) { offsetY = offsetY + 20; frame.origin.y = viewBounds.size.height - 60 -(j*60)-offsetY; } else { frame.origin.y = viewBounds.size.height - 60 -(j*60)-offsetY; } if((i)%3==0) { offsetX = offsetX +20; frame.origin.x = 10+(i*60)+offsetX; } else { frame.origin.x = 10+(i*60)+offsetX; } textField.frame = frame; [textFields addObject:textField]; } offsetX = 0; } ... }
Als erstes wird mit setContentSize
die Größe des NSWindow
angepaßt, so dass das Spielfeld in der View ausreichend Platz hat. Da sich die mainView
im Window befindet, läßt sich die Größe der NSView
durch den Aufruf der Methode bounds
abfragen. Dieser Wert wird benötigt, um die NSTextFields
in der View zu platzieren.
Die NSTextFields
werden in einem NSMutableArray
gespeichert. Obwohl es sich bei dem Spielfeld um eine "zweidimensionale Struktur" handelt, kann diese problemlos in dem eindimensionalen Array abgelegt werden. Mit folgender Hilfsfunktion ist ein Zugriff per row
und column
(Zeile, Spalte) möglich:
-(int) getValueFramMatrix:(int)row:(int)column { return [[textFields objectAtIndex:row*9+column] intValue]; }
Da 81 Felder für das Sudoku-Spielfeld erzeugt werden sollen, werden zwei for
-Schleifen dafür verwendet, die jeweils bis 9 zählen. (Es wäre auch eine Lösung mit einer Schleife denkbar, die aber durch die dann notwendige index-Berechnung schwerer zu verstehen wäre.)
In der inneren Schleife wird als erstes ein NSTextField
erstellt und der Inhalt auf einen Leerstring gesetzt. Anschließend wird die Größe (frameSize
), der Rahmen (Bordered
), die Schriftart (Font
) und die Textausrichtung (Alignment
) festgelegt.
Mit der Anweisung [mainView addSubview:textField];
wird in Zeile 22 das NSTextField
der View hinzugefügt.
Zum Schluß muß noch die Position innerhalb der View festgelegt werden. Dazu fragt man den frame
des NSTextFields
ab und setzt in diesem die Werte für die x- und y-Position. Da zwischen jedem "Neunerblock" noch eine Linie gezeichnet werden soll, muß mit dem offsetX
und offsetY
ein bißchen Extraplatz geschaffen werden.
Abschließend wird das positionierte NSTextField
in dem NSArray
mit [textFields addObject:textField];
gespeichert.
Trennlinien mit NSBox
erstellen
Als nächstes werden die Trennlinien zwischen den Felderblöcken eingefügt. Das funktioniert prinzipiell nach dem gleichen Schema.
Es wird eine NSBox
erstellt und das Aussehen mittels BorderWidth
und FrameSize
angepaßt. Anschließend wird die Position (frame
) festgelegt und die line
in der mainView
hinzugefügt.
NSBox *line = [[NSBox alloc] init]; [line setBorderWidth:1]; [line setFrameSize:NSMakeSize(2, 600)]; CGRect frame = line.frame; frame.origin.x = 215; frame.origin.y = 60; line.frame = frame; [mainView addSubview:line]; line = [[NSBox alloc] init]; [line setBorderWidth:1]; [line setFrameSize:NSMakeSize(2, 600)]; frame = line.frame; frame.origin.x = 415; frame.origin.y = 60; line.frame = frame; [mainView addSubview:line]; line = [[NSBox alloc] init]; [line setBorderWidth:1]; [line setFrameSize:NSMakeSize(600, 2)]; frame = line.frame; frame.origin.x = 20; frame.origin.y = 265; line.frame = frame; [mainView addSubview:line]; line = [[NSBox alloc] init]; [line setBorderWidth:1]; [line setFrameSize:NSMakeSize(600, 2)]; frame = line.frame; frame.origin.x = 20; frame.origin.y = 465; line.frame = frame; [mainView addSubview:line];
NSButton
als SubView
Was jetzt noch fehlt, ist der NSButton
mit der Beschriftung Solve. Das Prinzip ist das gleiche, wie bei den anderen Komponenten. Button erstellen, Werte setzen, Position festlegen und zur View hinzufügen.
NSButton* solve = [[NSButton alloc]init]; [solve setFrameSize:NSMakeSize(100, 30)]; [solve setTitle:@"Solve"]; [solve setStringValue:@"Solve"]; [solve setBezelStyle:NSRoundedBezelStyle]; frame = solve.frame; frame.origin.x = 265; frame.origin.y = 20; solve.frame = frame; [mainView addSubview:solve];
IBAction erstellen
Da der Button nach dem Klick auf Solve die Methode doClickSolve
aufrufen soll, legen wir eine IBAction
programmatisch an. Der Button besitzt dafür die Methode setAction
. Diese wird mit dem Parameter @selector(doClickSolve:)
aufgerufen.
[solve setAction:@selector(doClickSolve:)];
Wenn nun der Benutzer auf den Button klickt, dann wird versucht die Methode doClickSolve:
aufzurufen, die folgendermaßen aussehen könnte:
-(IBAction) doClickOk:(id)sender { //Solve my Sudoku }
Navigation mit Tab-Taste
Damit das erste Feld beim Starten der Applikation selektiert ist und man dort direkt einen Wert eintragen kann, muss folgene Anweisung ergänzt werden.
// Select first field NSTextField *firstField = [textFields objectAtIndex:0]; [[firstField window] makeFirstResponder:firstField];
Durch diesen Aufruf wird der firstResponder
gesetzt.
Da die Eingabe von Werte enorm erleichtert wird, wenn man mittels der Tab-Taste durch das Spielfeld navigieren kann, sollte diese Erweiterung noch ergäzt werden. Um diese Funktionalität einzubauen, ist es notwendig einmal über alle NSTextFields
in der Variable textFields
zu iterieren und den Vorgänger und Nachfolder mittels NextKeyView
zu verbinden.
//Jump with TAB through all NSTextFields for(int i = 1; i<[textFields count];i++) { NSTextField* f1 = [textFields objectAtIndex:(i-1)]; NSTextField* f2 = [textFields objectAtIndex:(i)]; [f1 setNextKeyView:f2]; }
Nach dem Abspeichern läßt sich die Applikation kompilieren und starten.