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

Von Stefan Haubold vor 77 Tagen hinzugefügt (0 Kommentare)

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

Von Stefan Haubold vor 411 Tagen hinzugefügt (0 Kommentare)

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

Von Thomas Dohmke vor 537 Tagen hinzugefügt (0 Kommentare)

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

Von Thomas Dohmke vor 550 Tagen hinzugefügt (0 Kommentare)

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;
}

[Revision 7d181d7d]

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

Von Thomas Dohmke vor 551 Tagen hinzugefügt (0 Kommentare)

1 2 Ältere Artikel »

Auch abrufbar als: Atom

Twitter

Uns auf Twitter verfolgen: