iPhone Code Schnipsel: Maximale Anzahl Zeichen im UITextField begrenzen
Gegeben
Man hat ein UITextField und möchte verhindern, dass ein User mehr als X Zeichen eingeben kann.
Gesucht
Eine Lösung ohne UIAlertView. Es soll nur verhindert werden, dass weitere Zeichen eingegeben werden.
Lösung
Erstmal zwei Konstanten definieren:
static const NSInteger kUserNameMaxLength = 20;
static const NSInteger kTextFieldNameTag = 1;
Dann entsprechend dem UITextField das Tag zuweisen (static const NSInteger) und den delegate setzen:
textField.tag = kTextFieldTagLimited;
textField.delegate = self;
Dann lässt sich das Problem mit dieser UITextFieldDelegate Methode ziemlich einfach lösen:
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string {
if (textField.tag == kTextFieldTagLimited) {
NSString *testString = [textField.text stringByReplacingCharactersInRange:range withString:string];
if ([testString length] > kUserNameMaxLength) {
textField.text = [textField.text substringToIndex:kUserNameMaxLength];
return NO;
}
}
return YES;
}
Schlüsselwörter: iphone, objective-c, programmierung, schnipsel
iPhone Code Schnipsel: Clang Static Analyzer
Gegeben
Man hat die ersten ViewController fertig, einiges an Logik geschrieben, so dass die Applikation eigentlich ganz gut funktionieren sollte. Der erste Start endet aber im Debugger und nach kurzem Prüfen ist man sich sicher, dass man irgendwo einen Bug im Object-Lifecycle hat - also ein Retain/Release/Autorelease zu viel oder zu wenig.
Gesucht
Ein schneller Weg, diese Fehler zu finden, ohne seinen eigenen Code noch einmal komplett durchgehen zu müssen.
Lösung
Der Clang Static Analyzer: http://clang-analyzer.llvm.org/
Einfach den letzten Build downloaden und entpacken. Am komfortabelsten ist es, wenn man das Verzeichnis gleich dem PATH hinzufügt. Dann in Xcode ein "Clean All Targets" ausführen und auf der Konsole in das Projektverzeichnis wechseln. Dort gibt man folgendes Kommando ein:
scan-build -k -V xcodebuild -configuration Debug -sdk iphonesimulator3.0
Wenn alles klappt, sollte Clang nun das Projekt scannen und am Ende eine Seite im Default Browser öffnen. Dort findet man dann eine Übersicht aller gefunden Fehler, Bugs und Verstöße gegen Coding Conventions. Die Punkte geht man einzeln durch und bekommt wunderbar erklärte Problembeschreibungen. Das sieht dann z.B. so aus:
Als Folge stehen die Chancen gut, dass der nächste Start der neuen App problemlos klappt.
Schlüsselwörter: iphone, objective-c, programmierung, schnipsel, xcode
Kurztip: Fehlermeldung "Unrecognized Selector" in Xcode
Von Zeit zu Zeit wird ein Fehler während des Programmierens in Xcode mit der folgenden Fehlermeldung beim Ablauf des Programms bestraft:
2009-03-18 18:51:12.734 PlayGround[51029:20b] *** -[NSCFString someString]: unrecognized selector sent to instance 0xa05b1328
Der Stacktrace des Debuggers beinhaltet dabei nicht die Stelle, an der der Fehler verursacht wird. Abhilfe schafft folgende Zeile in der Datei ~/.gdbinit:
fb objc_exception_throw
Der Befehl definiert einen Breakpoint für die Funktion objc_exception_throw, die bei jeder ausgelösten Exception aufgerufen wird. Dadurch bleibt der Debugger an einer Stelle stehen, an der der Verursacher noch im Stacktrace zu finden ist.
Schlüsselwörter: kurztip, objective-c, xcode
iPhone Code Schnipsel: Subviews umdrehen
Gegeben
Ein Fenster mit zwei Subviews:
[window addSubview:view1];
[window addSubview:view2];
Gesucht
Zwischen den Views wechseln und dabei eine Animation anzeigen, die ein Umdrehen symbolisiert.
Lösung
CGContextRef context = UIGraphicsGetCurrentContext();
[UIView beginAnimations:nil context:context];
[UIView setAnimationTransition:UIViewAnimationTransitionFlipFromLeft forView:window cache:YES];
[UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
[UIView setAnimationDuration:1.0];
[window exchangeSubviewAtIndex:1 withSubviewAtIndex:0];
[UIView commitAnimations];
Will man von rechts nach links drehen, nimmt man UIViewAnimationTransitionFlipFromRight statt dem obigen UIViewAnimationTransitionFlipFromLeft.
Schlüsselwörter: iphone, objective-c, programmierung, schnipsel
Unit-Testing in Objective-C, Teil 3
Im dritten Teil unserer Serie über Unit-Testing in Objective-C wollen wir das Beispielprojekt aus Teil 1 fortsetzen und dabei die Erstellung von Mock-Objekten mit OCMock kennen lernen. Der Beispielcode steht wieder als Git-Repository zur Verfügung, welches per
git clone git://www.komprovisation.de/objcut1.git
lokal geklont oder auf der Projektseite durchstöbert werden kann. Zudem ist im Artikel an dedizierten Stellen die jeweilige Revision vermerkt.
Gegeben
Aufbauend auf der letzten Revision des ersten Teils haben wir das Modell User bereits um eine einfache Datenbankanbindung erweitert. Als Datenbank wird SQLite zusammen mit der Bibliothek PLDatabase verwendet. Letztere hat den Vorteil, dass die Aufruf an SQLite gekapselt werden und damit ein Wechsel auf ein anderes Datenbanksystem leichter möglich sein sollte. Die Klasse User verwendet die Datenbank über die Hilfsklasse UserDatabase, die im Wesentlichen die folgenden zwei Methoden zur Verfügung stellt:
- (User *)findWithEmail:(NSString *)email {
[database open];
User *user = nil;
NSObject<PLResultSet> *results;
results = [database executeQuery: @"SELECT * FROM users WHERE email = ?", email];
if ([results next]) {
user = [[User alloc] init];
user.email = [results stringForColumn:@"email"];
user.hashedPassword = [results stringForColumn:@"hashed_password"];
}
[results close];
[database close];
return [user autorelease];
}
- (BOOL)saveWithEmail:(NSString *)email hashedPassword:(NSString *)hashedPassword {
[database open];
NSObject<PLPreparedStatement> *statement;
statement = [database prepareStatement: @"INSERT INTO users (email, hashed_password) VALUES (?, ?)"];
[statement bindParameters: [NSArray arrayWithObjects: email, hashedPassword, nil]];
BOOL result = [statement executeUpdate];
[statement close];
[database close];
return result;
}
Die Methode findWithEmail: sucht einen Benutzer anhand der E-Mail-Adresse und liefert eine Instanz des Objektes User zurück. Die Methode saveWithEmail:hashedPassword: speichert einen Benutzer mit seiner E-Mail-Adresse und dem verschlüsselten Password. Die Verschlüsselung des Passwortes ist unabhängig von der Datenbank und erfolgt daher in User mittels der Methode encrypt:.
+ (NSString *)encrypt:(NSString *)password {
unsigned char hash[CC_SHA256_DIGEST_LENGTH];
CC_SHA256([password UTF8String],
[password lengthOfBytesUsingEncoding:NSUTF8StringEncoding],
hash);
NSMutableString *hashedPassword;
hashedPassword = [NSMutableString stringWithCapacity:CC_SHA256_DIGEST_LENGTH];
for (int i = 0; i < CC_SHA256_DIGEST_LENGTH; ++i) {
[hashedPassword appendString:[NSString stringWithFormat:@"%02x", hash[i]]];
}
return hashedPassword;
}
Gesucht
Ziel ist es nun, die Methode loginWithEmail:password:
- (BOOL)loginWithEmail:(NSString *)login password:(NSString *)password {
return ([login isEqualToString:@"thomas@dohmke.de"] &&
[password isEqualToString:@"foobar"]);
}
so abzuändern, dass diese eine Klassenmethode ist und eine Instanz von User zurückliefert, sofern die Authentifizierung mit E-Mail-Adresse und Passwort erfolgreich war. E-Mail und Passwort sollen dabei mit den Werten in der Datenbank verglichen werden, d.h. die Methode muss im Gegensatz zur bisherigen Fake-Version korrekt funktionieren.
Lösung
Im ersten Schritt wandeln wir die beiden Testmethoden in UserTest.m so ab, dass sie der gewünschten Signatur entsprechend. Zur Erinnerung nachfolgend zunächst die bisherige Version:
-(void)testValidLogin {
STAssertEquals([user loginWithEmail:@"thomas@dohmke.de" password:@"foobar"],
YES, @"should return YES");
}
-(void)testInvalidLogin {
STAssertEquals([user loginWithEmail:@"thomas@dohmke.de" password:@"wrong"],
NO, @"should return NO");
}
Die Methode soll in eine Klassenmethode umgewandelt werden, daher müssen wir sie statt an die Instanz user an die Klasse User senden. Außerdem soll sie eine Instanz von User zurückliefern, entsprechend sind die Aufrufe von STAssertEquals durch STAssertNotNil bzw. STAssertNil zu ersetzen:
- (void)testValidLogin {
STAssertNotNil([User loginWithEmail:@"thomas@dohmke.de" password:@"foobar"],
@"Should return user object.");
}
- (void)testInvalidLogin {
STAssertNil([User loginWithEmail:@"thomas@dohmke.de" password:@"wrong"],
@"Should return nil.");
}
Bei der Ausführung schlagen die Tests erwartungsgemäß fehl, da die erwartete Signatur der Methode nicht mit der Implementierung übereinstimmt. Entsprechend ändern wir die Deklaration in User.h
+ (User *)loginWithEmail:(NSString *)login password:(NSString *)password;
und passen die Definition in User.m entsprechend an:
+ (User *)loginWithEmail:(NSString *)login password:(NSString *)password {
User *user = [User findWithEmail:login];
if ([[User encrypt:password] isEqualToString:user.hashedPassword]) {
return user;
}
else {
return nil;
}
}
Die Implementierung sucht den Benutzer mittels der Methode findWithEmail:, verschlüsselt den Parameter password und vergleicht das Ergebnis mit dem verschlüsselten Passwort in der von findWithEmail: gefundenen Instanz. [Revision 13222fee]
Die Tests werden nun bestanden, allerdings ergibt sich ein neues Problem: Damit die Methode den Benutzer überhaupt in der Datenbank finden kann, müssen wir diesen dort speichern. Dies erfolgt bereits in der Methode setUp des Testfalls:
- (void)setUp {
user = [User userWithEmail:@"thomas@dohmke.de" password:@"foobar"];
STAssertEquals([user save], YES, @"save failed.");
}
Da das Speichern jedoch in derselben Datenbank erfolgt, die auch vom eigentlichen Programm benutzt wird, würden wir bei einem echten System möglicherweise reale Benutzerdaten überschreiben. Das ist unschön. Zudem wird der Benutzer mit jeder Testmethode neu angelegt wird, wie wir uns leicht auf der Konsole überzeugen können:
$ sqlite3 objcut1.db
SQLite version 3.4.0
Enter ".help" for instructions
sqlite> select * from users;
1|thomas@dohmke.de|c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2
2|thomas@dohmke.de|c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2
3|thomas@dohmke.de|c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2
4|thomas@dohmke.de|c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2
Hier kommt nun OCMock ins Spiel.
Bessere Lösung
OCMock erlaubt uns, die Klasse UserDatabase durch einen Dummy zu ersetzen und in Folge zu verhindern, dass tatsächlich in die Datenbank geschrieben wird. Entsprechend wird UserDatabase selbst nicht mehr getestet, was in Ordnung geht, da wir in UserTest.m nur die Klasse User testen wollen.
Zuerst installieren wir OCMock, in dem wir es von seiner Homepage runterladen (aktuell ist derzeit Version 1.29), das Archiv entpacken und das Verzeichnis OCMock.framework nach /Library/Frameworks kopieren. Dann klicken wir unter Groups & Files rechts auf Frameworks > Linked Frameworks, Menüpunkt Add > Existing Frameworks, suchen das gerade kopierte Verzeichnis und bestätigen mit Add.
Als nächstes fügen wir der Testklasse in UserTest.h eine Instanzvariable für das Mock-Objekt hinzu:
@interface UserTest : SenTestCase {
// ...
id databaseMock;
}
In UserTest.m importieren wir OCMock
#import <OCMock/OCMock.h>
und definieren eine neue Methode, die das Mock-Objekt erstellt:
- (void)setUpMockDatabase {
BOOL yes = YES;
NSValue *wrappedValue = [NSValue valueWithBytes:&yes objCType:@encode(BOOL)];
databaseMock = [OCMockObject mockForClass:[UserDatabase class]];
[[[databaseMock stub] andReturnValue:wrappedValue] saveWithEmail:user.email hashedPassword:user.hashedPassword];
[[[databaseMock stub] andReturn:user] findWithEmail:user.email];
[User setDatabase:databaseMock];
}
Die ersten zwei Zeilen packen den booleschen Wert YES in eine Instanz der Klasse NSValue ein. In den folgenden drei Zeilen wird das Mock-Objekt für die Klasse UserDatabase mit zwei Stub-Methoden erstellt. Die Signaturen entsprechend dabei den Deklarationen in UserDatabase.h, wobei saveWithEmail:hashedPassword: stets YES und findWithEmail: die in setUp erstellte Instanz von User zurückgeben soll. In der letzten Zeile wird das Mock-Objekt der Klasse User übergeben, die dieses in Folge für alle Datenbankzugriffe verwendet.
Schließlich rufen wir setUpMockDatabase in der setup-Methode auf:
- (void)setUp {
user = [User userWithEmail:@"thomas@dohmke.de" password:@"foobar"];
[self setUpMockDatabase];
STAssertEquals([user save], YES, @"save failed.");
}
Fertig. Die Tests werden weiterhin bestanden, greifen aber nicht mehr auf die Datenbank zu. [Revision aeb659e1]
Erster Nachgang
Neben stub können Stub-Methoden auch mit expect definiert werden. Der Unterschied besteht darin, dass eine mit expect definierte Methode genau einmal aufgerufen werden muss, während die Anzahl der Aufrufe bei "stub" gleichgültig ist, d.h. auch kein Aufruf ist erlaubt. Will man mehr als einen Aufruf kontrollieren, ist das expect-Statement entsprechend oft zu wiederholen.
Als Beispiel verändern wir setUpMockDatabase wie folgt
- (void)setUpMockDatabase {
// ...
[[[databaseMock expect] andReturnValue:wrappedValue] saveWithEmail:user.email hashedPassword:user.hashedPassword];
// ...
}
und verifizieren die Anzahl der Aufruf in tearDown:
- (void)tearDown {
[databaseMock verify];
[user release];
}
Die Methode verify überprüft nach jedem Test, ob die mit expect definierte Stub-Methoden jeweils genau einmal aufgerufen wurde (was im Beispiel der Fall ist, da vor jedem Test in setUp die Instanz user gespeichert wird). [Revision 11065e8d]
Zweiter Nachgang
Wie würde die Lösung aussehen, wenn man kein Mock-Objekt verwenden will, sondern die Anbindung der Datenbank in UserDatabase bewusst mittesten möchte? SQLite erlaubt die Erstellung einer Datenbank im Hauptspeicher, so dass wir eine neue Methode setUpTestDatabase in UserTest.m wie folgt definieren
- (void)setUpTestDatabase {
userDatabase = [[UserDatabase alloc] initWithPath:@":memory:"];
[userDatabase.database executeUpdate: @"DROP TABLE users;"];
STAssertTrue([userDatabase.database executeUpdate: @"CREATE TABLE users (id INTEGER PRIMARY KEY NOT NULL, email VARCHAR(255), hashed_password VARCHAR(255));"], @"");
[User setDatabase:userDatabase];
}
und statt setUpMockDatabase in setUp aufrufen könnten:
- (void)setUp {
user = [User userWithEmail:@"thomas@dohmke.de" password:@"foobar"];
[self setUpTestDatabase];
STAssertEquals([user save], YES, @"save failed.");
}
Es ist zu beachten, dass die SQL-Befehle in setUpTestDatabase hier nur beispielhaft aufgeführt werden, im realen Leben würde man die Klasse UserDatabase um entsprechende Methoden erweitern, die diese kapseln und damit Dopplungen im Code vermeiden.
Schlüsselwörter: objective-c, programmierung, testen, xcode
Auch abrufbar als: Atom
Schlüsselwörter
- berlin (2)
- blog (5)
- browser (2)
- cocoaheads (5)
- dropbox (1)
- git (7)
- idisk (1)
- iphone (28)
- javascript (2)
- kurztip (4)
- linktips (17)
- mac (9)
- macruby (1)
- objective-c (8)
- ortung (1)
- programmierung (22)
- rails (1)
- railsconf (7)
- ruby (6)
- ruby on rails (7)
- schnipsel (14)
- server (2)
- spiele (1)
- statistiken (3)
- stuttgart (3)
- testen (4)
- tidy (1)
- versionskontrolle (5)
- wwdc (1)
- xcode (9)
- xml (1)
