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.

Create New Project

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.