L'etat, c'est moi
Mere Complexities sells the consulting and development services of me, Paul Wilson.
Conferences
Archive
Twitterlink available on Apple's Appstore
Talking of Twitter clients, my first iPhone application – Twitterlink – is now available on the AppStore. It is a simple beast with one purpose: it enables posting shortened URLs from iPhone’s Safari web-browser to Twitter.
You can download Twitterlink from the app store here (app store link), or read a little more about it here.
It’s free for now. I will be putting the price up to (gasp) £0.56 ($0.99) soon.
iPhone TDD with Rbiphonetest II - Posting to Twitter
Earlier I wrote a brief tutorial about getting started with Rbiphonetest we test-drove saving and reloading Twitter account details. Now let’s go for something a bit more ambitious: sending a tweet.
We’ll start by checking a request is sent to the Twitter api url for tweeting.
def test_tweets_are_sent_to_the_correct_twitter_api_url
@testee.tweet "hello matey"
assert_equal "http://twitter.com/statuses/update.xml",OSX::NSURLConnection.last_request_url
end
We’ll need to trick-up NSURLConnection to record the last request url.
class OSX::NSURLConnection
class << self
attr_reader :last_request_url;
define_method 'connectionWithRequest:delegate:' do |request, delegate|
@last_request_url = request.URL.absoluteString
end
end
end
Something to note here we used define_method with the actual Objective-C method name, colons and all. There seems to be a RubyCocoa glitch that means that you need to use the exact method name, when monkey-patching methods. Usually RubyCocoa supports replacing colons in Objective-C method names with underscores: we would expect to be able to use connectionWithRequest_delegate as a Ruby equivalent, as indeed we can when calling the method. As ’:’ is not legal to use in a Ruby def statement, we are using define_method which is much more liberal.
Now for the minimum implementation to make the test pass.
-(void) tweet:(NSString*)message{
NSURLRequest *request = [NSURLRequest requestWithURL:
[NSURL URLWithString:@"http://twitter.com/statuses/update.xml"]];
[NSURLConnection connectionWithRequest:request delegate:nil];
}
That was easy, and we’re getting somewhere. Ok what else do we need to post a Tweet? Well it has to be a HTTP POST and contain the message in the parameter status.
def test_tweets_are_post_with_message_the_status_parameter
@testee.tweet "hello matey"
assert_equal "POST", OSX::NSURLConnection.last_request_method
assert_equal "status=hello matey", OSX::NSURLConnection.last_request_body
end
Let’s open up bit more of a window into the soul of NSURLConnection.
class OSX::NSData
def from_nil_terminated_str
i = 0
loop do
return bytes.bytestr(i) if bytes.int8_at(i) == 0
i+=1
end
end
end
class OSX::NSURLConnection
class << self
attr_reader :last_request_url, :last_request_method,
:last_request_body;
define_method 'connectionWithRequest:delegate:' do |request, delegate|
@last_request_url = request.URL.absoluteString
@last_request_method = request.HTTPMethod
@last_request_body = request.HTTPBody.from_nil_terminated_str if request.HTTPBody
end
end
end
We needed to do a bit of work converting the HTTPBody back to a string. Our solution isn’t bullet-proof as it’s UTF8 encoded, but as long as we stick to single byte UTF8 characters in our test data we’ll be fine.
In the implementation we need to use to a NSMutableURLRequest so we can set the HTTPMethod and HTTPBody properties.
-(void) tweet:(NSString*)message{
NSMutableURLRequest *request =
[NSMutableURLRequest requestWithURL:
[NSURL URLWithString:@"http://twitter.com/statuses/update.xml"]];
request.HTTPMethod = @"POST";
request.HTTPBody = [[NSString stringWithFormat:@"status=%@",message]
dataUsingEncoding:NSUTF8StringEncoding];
[NSURLConnection connectionWithRequest:request delegate:nil];
}
So who’s sending this Tweet? We need to authenticate ourselves; this is going to be done by an asynchronous callback. We’re going to need to do a bit more tricking things out: we’ll record the last delegate used by NSURLConnection.
class OSX::NSURLConnection
class << self
attr_reader :last_request_url, :last_request_method, :last_request_body, :delegate;
define_method 'connectionWithRequest:delegate:' do |request, delegate|
@last_request_url = request.URL.absoluteString
@last_request_method = request.HTTPMethod
@last_request_body = request.HTTPBody.from_nil_terminated_str if request.HTTPBody
@last_delegate = delegate
end
end
end
And we’ll need to use our own special authentication challenge to check what gets called.
class StubAuthenticationChallenge < OSX::NSURLAuthenticationChallenge
attr_reader :previousFailureCount, :sender
class StubSender
attr_reader :credential, :challenge, :cancelled_challenge
def useCredential_forAuthenticationChallenge credential, challenge
@credential = credential.retain.autorelease if credential
@challenge = challenge
end
def cancelAuthenticationChallenge challenge
@cancelled_challenge = challenge
end
end
def initialize
@previousFailureCount = 0
@sender = StubSender.new
end
def credential_used
@sender.credential
end
def challenge_used_with_credential
@sender.challenge
end
def cancelled?
@sender.cancelled_challenge
end
end
Note that our stub actually extends NSURLAuthenticationChallenge. We don’t need to do that our stub only returns pointers to Objective-C objects. If we have a method that returns a C structures – such as NSInteger, BOOL, and NSRange – we need to provide RubyCocoa a hint to what we are returning by overriding the class that defines that method: in this case previousFailureCount, which we will be using later, returns NSInteger.
Also worth noting is that we call retain on the credential in the StubSender_ method useCredential_forAuthenticationChallenge: this is to keep it around long enough for us to check its values.
def test_username_and_password_used_in_response_to_authentication_challenge
@testee.username = 'rita'
@testee.password = 'mysecret'
@testee.tweet "hi"
challenge = StubAuthenticationChallenge.alloc
assert OSX::NSURLConnection.last_delegate
OSX::NSURLConnection.last_delegate.connection_didReceiveAuthenticationChallenge nil,
challenge
assert_equal challenge, challenge.challenge_used_with_credential
assert challenge.credential_used
assert_equal "rita", challenge.credential_used.user
assert_equal "mysecret", challenge.credential_used.password
end
Confused? Maybe. Let’s walk through that a little.
- When our Objective-C code creates the connection asynchronously, we now need it to pass a ‘delegate’ to the connectionWithRequest:delegate: method on NSURLConnection; this delegate receives the asynchronous callbacks relaying the chaninging state of the connection.
- In our test code we are now monkey patching to, amongst other things, record the delegate passed to connectionWithRequest:delegate:.
- Our test can then call methods on the delegate to check the reaction of our code to callbacks: in this case the connection:didReceiveAuthenticationChallenge callback.
- We’ve stubbed out the StubAuthenticationChallenge, and record the challenge response to its sender.
(Note: as StubAuthenticationChallenge extends an Objective-C class we create using alloc instead of new: another RubyCocoa oddity.)
Let’s make that pass: we need a delegate to respond to the challenge.
@interface TweetDelegate : NSObject{
NSString *username;
NSString *password;
}
@property(nonatomic, retain) NSString* username;
@property(nonatomic, retain) NSString* password;
@end
@implementation TweetDelegate
@synthesize username;
@synthesize password;
+(id)tweetDelegateWithUsername:(NSString*) username andPassword:(NSString*) password{
TweetDelegate *result = [[TweetDelegate alloc] autorelease];
result.username = username;
result.password = password;
return result;
}
- (void)connection:(NSURLConnection *)connection
didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
NSURLCredential *credential = [[NSURLCredential alloc]
initWithUser:self.username
password:self.password
persistence:NSURLCredentialPersistenceForSession];
[[challenge sender] useCredential:credential forAuthenticationChallenge:challenge];
[credential release];
}
@end
And we’ll pass that delegate into the NSURLConnection creation.
-(void) tweet:(NSString*)message{
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:
[NSURL URLWithString:@"http://twitter.com/statuses/update.xml"]];
request.HTTPMethod = @"POST";
request.HTTPBody = [[NSString stringWithFormat:@"status=%@",message]
dataUsingEncoding:NSUTF8StringEncoding];
[NSURLConnection connectionWithRequest:request delegate:
[TweetDelegate tweetDelegateWithUsername:self.username andPassword:self.password]];
}
Now we need to make sure that we only attempt the authentication once: if we have the wrong credentials the first time they are going to stay wrong no matter how many times we try to authenticate against them.
def test_only_attempts_to_authenticate_once_against_an_authentication_challenge
challenge = StubAuthenticationChallenge.alloc
challenge.previousFailureCount = 1
@testee.username = 'rita'
@testee.password = 'mysecret'
@testee.tweet "yo"
OSX::NSURLConnection.last_delegate.connection_didReceiveAuthenticationChallenge nil, challenge
assert challenge.credential_used.nil?
assert challenge.cancelled?
end
-(void)connection:(NSURLConnection *)connection
didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
if ([challenge previousFailureCount] == 0){
NSURLCredential *credential = [[NSURLCredential alloc]
initWithUser:self.username
password:self.password
persistence:NSURLCredentialPersistenceForSession];
[[challenge sender] useCredential:credential forAuthenticationChallenge:challenge];
[credential release];
}
else{
[[challenge sender] cancelAuthenticationChallenge:challenge];
}
}
Now we’re tweeting!
Or are we? We don’t know as we’re using NSURLConnection asynchronously and we’re not hanging around long enough to find out what happened. Let’s get some feedback. We’ll start with an Objective-C protocol to be notified.
@protocol TweetObserver
-(void)tweetSucceeded;
-(void)tweetFailedWithError:(NSError*) error;
-(void)tweetFailedAuthentication;
@end
Ok, ok. I know that idiomatically we ought to call the protocaol TweetDelegate, but I just can’t bring myself to do that. Observer, Listener, even Callback – yes. But in what way is this a delegate? That’s right: in no way.
Now I’ve got that off my chest let’s stub that out and write the test to show us being notified of a successful tweet: we should get a connectionDidFinishLoading callback on success.
def test_should_be_notified_of_a_successful_tweet
@testee.tweet_andNotifyObserver "hey", @tweet_observer
OSX::NSURLConnection.last_delegate.connectionDidFinishLoading nil
assert @tweet_observer.tweet_succeeded
end
Ok, failing: we’ll change the tweet method to take the observer and pass it to the connection delegate.
-(void) tweet:(NSString*)message andNotifyObserver:(id<TweetObserver>) observer{
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL
URLWithString:@"http://twitter.com/statuses/update.xml"]];
request.HTTPMethod = @"POST";
request.HTTPBody = [[NSString stringWithFormat:@"status=%@",message]
dataUsingEncoding:NSUTF8StringEncoding];
[NSURLConnection connectionWithRequest:request delegate:
[TweetDelegate tweetDelegateWithUsername:self.username
password:self.password andTweetObserver:observer]];
}
Now let’s wire up the observer to the delegate.
@interface TweetDelegate : NSObject{
NSString *username;
NSString *password;
id<TweetObserver> tweetObserver;
}
@property(nonatomic, retain) NSString* username;
@property(nonatomic, retain) NSString* password;
@property(nonatomic, retain) id<TweetObserver> tweetObserver;
@end
@implementation TweetDelegate
@synthesize username;
@synthesize password;
@synthesize tweetObserver;
+(id)tweetDelegateWithUsername:(NSString*)username password:(NSString*)password
andTweetObserver:(id<TweetObserver>) observer{
TweetDelegate *result = [[TweetDelegate alloc] autorelease];
result.username = username;
result.password = password;
result.tweetObserver = observer;
return result;
}
-(void)connectionDidFinishLoading:(NSURLConnection*)connection{
[self.tweetObserver tweetSucceeded];
}
.......
@end
And now to deal with failures.
def test_listener_notified_of_failure
error = OSX::NSError.alloc
@testee.tweet_andNotifyObserver "oi!", @tweet_observer
OSX::NSURLConnection.last_delegate.connection_didFailWithError nil, error
assert_equal error, @tweet_observer.tweet_error
assert !@tweet_observer.tweet_succeeded
end
def test_user_notified_of_authentication_failure
error = OSX::NSError.errorWithDomain_code_userInfo("NSURLErrorDomain",
OSX::NSURLErrorUserCancelledAuthentication, nil)
@testee.tweet_andNotifyObserver "hi!", @tweet_observer
OSX::NSURLConnection.last_delegate.connection_didFailWithError nil, error
assert @tweet_observer.tweet_failed_authentication
assert !@tweet_observer.tweet_error
assert !@tweet_observer.tweet_succeeded
end
Impementation:
@implementation TweetDelegate
.....
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{
if ([error code] == NSURLErrorUserCancelledAuthentication){
[self.tweetObserver tweetFailedAuthentication];
}else{
[self.tweetObserver tweetFailedWithError:error];
}
}
@end
That’s about it. We’ve now test driven a functional increment: the ability to send a tweet. We’re not really done-done, though: there are still a few questions hanging around in the ether.

So we’ve gone to the trouble of hand-rolling all this stubbing around NSURLConnection and friends: why not use one of the popular Ruby mock-object framework, like FlexMock or Mocha? While I’m not keen on mocks, this isn’t the primary reason. After all we’ve ended up writing mostly interaction tests against NSURLConnection and friends, and Flexmock, at least, is somewhat agnostic about whether it is a mocking or a stubbing framework.
We mainly have not used a mocking framework in order to limit the number of moving parts in the example. This is experimental stuff: it is challenging enough to track down anomalies to coding mistakes, misunderstandings, or RubyCocoa glitches; throwing another framework, particularly one with the potential to react in interesting and unexpected ways with RubyCocoa, would make things even more difficult.
Consider also that the stubbing code that we have written is generally useful: it has the potential to be extracted into a library (gem) to be reused in different projects.

The mainstream of Ruby, particularly Rails, development has gone all BDD these days and RBIPhonetest does support RSpec. We have used TestUnit because I’m old-school. Expect to see some Shoulda in later posts, however.

If you’ve been following closely you may have noticed that we’ve added the functionality to send a tweet, with all that HTTP shenanigans, to the same class responsible for persisting the account details. Both the test and the productions code suffer from an unfortunate mix of concerns. But do not despair. Remember the old XP maxim:
Make it work, then make it right
In the next thrilling episode we’ll refactor the production code, supported by the tests; we’ll also refactor the tests, supported by the production code.
Unit Testing iPhone Development with Rbiphonetest
I’ve been doing Phone development recently, and have a little application ready to send to the App Store. Although it’s not been long since the NDA was lifted, there is quite a lot of information out there. I’ve been mostly using:-
- Apple’s own bundled documentation, which is very good.
- the Pragmatic iPhone SDK Development, which is in Beta, and the associated forum.
- StackOverflow, which has turned into an excellent source of information.
- The official Apple iPhone Development forums (registration needed), but only a little for some reason.
One area not so well covered is test driving iPhone development; the guys writing the Prag book seem very old-school, and TDD is not mentioned. Dr Nic to the rescue. The great thing about Dr Nic is that he doesn’t mind getting his hands dirty, just trying things out and coming up with partial solutions for the community to work with. I think that’s brilliant. One of Dr Nic’s little brain children is Rbiphonetest, which enables (currently limited) unit testing of iPhone Objective-C in ruby. It’s work-in-progress, but quite nifty.
Here’s a little getting started tutorial. I’m not going to run through creating a whole app – just testing a bit of it. If you’re coding along I’m assuming you set up for iPhone development (Mac OS 10.5 Leopard with the latest iPhone SDK). I’m also assuming your Ruby gem version is up to date (sudo gem update—system), so the first thing you need to do is install the gem
sudo gem install rbiphontest
Here I ran into a few problems installing on a new Mac: the gem dependencies weren’t properly sorted out, so I needed to install some other gems before successfully installing rbiphonetest; unfortunately I didn’t take a note, but I think they included hoe, and rubigen.
Were also going to want to run autotest. If you don’t already have it,
sudo gem install ZenTest
Let’s do some TDD. Twitter clients are the new Flashlight apps, so let’s start one of those. Fire up XCode and create an iPhone OS project. (For the purposes of this example it doesn’t matter what type; a Utility app is as good as any). From the terminal go to your new application directory and initialise rbiphontest.
rbiphontest .
This creates a few directories and scripts. It might also be a point where you encounter some dependency problems. If you do let me know and I’ll update this post; probably worthwhile dropping Dr Nic a line too.
Rbiphonetest only supports testing models just now, so let’s do that. We’ll need to store the user’s twitter account details, so let’s create a model for that. From the command line:
script/generate model TwitterAccount
That’s created an Objective-C header and implementation file in the Classes directory, and a Ruby test in test. Flip into XCode and add TwitterAccount.h and TwitterAccount.m to the project. (Menu Project > New Group “Classes”. Right-click on “Classes” and choose Add > Existing Files. )
We can compile and run the test using
rake
We get
1 tests, 0 assertions, 0 failures, 0 errors
Let’s do some TDD. XCode sucks, so I like to do most of the development in TextMate. Your mileage may vary, so open up “test/test_twitter_account.rb” in your own way.
mate .
This is what we have:
require File.dirname(__FILE__) + '/test_helper'
require "TwitterAccount.bundle"
OSX::ns_import :TwitterAccount
class TestTwitterAccount < Test::Unit::TestCase
def test_twitter_account_class_exists
OSX::TwitterAccount
end
end
The generated test just tests the compilation – fair enough. Let’s take a baby step: an account is going to need a username.
def test_twitter_account_has_username
testee = OSX::TwitterAccount.alloc
testee.username = 'marvin'
assert_equal "marvin", testee.username
end
Run rake and we get:
SX::OCMessageSendException: Can't get Objective-C method
signature for selector 'setUsername:'...
....
1 tests, 0 assertions, 0 failures, 1 errors
Yay, we have a failing test – we can write some production code.
TwitterAccount.h:
@interface TwitterAccount : NSObject {
@private
NSString *username;
}
@property(retain, nonatomic) NSString *username;
@end
TwitterAccount.m:
@implementation TwitterAccount
@synthesize username;
@end
Run rake and
1 tests, 1 assertions, 0 failures, 0 errors
Notice that we don’t have an easily affordable way of testing the “retain” and “nonatomic” attributes of the property; I really don’t think that matters.
We need a password as well. We don’t want to have to keep on running rake, though. Open up a new command shell(or tab) and change into the project directory.
autotest
Now everytime the a change is made to test, or code under test, any compilations required are performed and all the tests are run. With growl setup to give notifications, this is a very fluid way of TDDing Cocoa-Touch.
Ok, let’s add the test for the password.
def test_twitter_account_has_username_and_password
testee = OSX::TwitterAccount.alloc
testee.username = 'marvin'
testee.password = 'secret'
assert_equal "marvin", testee.username
assert_equal "secret", testee.password
end
It fails – let’s make it pass.
TwitterAccount.h:
@interface TwitterAccount : NSObject {
@private
NSString *username;
NSString *password;
}
@property(retain, nonatomic) NSString *username;
@property(retain, nonatomic) NSString *password;
@end
TwitterAccount.m:
@implementation TwitterAccount
@synthesize username;
@synthesize password;
@end
That was easy the easy stuff. Let’s try something a bit more challenging. We want to be able to save and reload the account details.
def test_account_details_may_be_saved_and_reloaded
testee = OSX::TwitterAccount.alloc
testee.username = 'victoria'
testee.password = 'secret'
testee.save
reloaded = OSX::TwitterAccount.alloc
reloaded.load
assert_equal "victoria", testee.username
assert_equal "secret", testee.password
end
Big step, but there’s a lot of boilerplate example for this kind of thing knocking around so let’s give it a go. We could add the following to TwitterAccount.m:
-(NSString*) accountPlistPath{
NSString *userDir =
[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)
objectAtIndex:0];
return [userDir stringByAppendingPathComponent:@"account.plist"];
}
-(void) save{
NSString *path = [self accountPlistPath];
NSDictionary *accountDetails =
[NSDictionary dictionaryWithObjectsAndKeys:self.username,
@"username", self.password, @"password", nil];
BOOL written = [accountDetails writeToFile:path atomically:YES];
NSLog(@"written: %d", written);
}
-(void) load{
NSDictionary *accountDetails =
[[NSDictionary alloc] initWithContentsOfFile:[self accountPlistPath]];
self.username = [accountDetails objectForKey:@"username"];
self.password = [accountDetails objectForKey:@"password"];
[accountDetails release];
}
That passes but there are issues. For one thing it violates one of Mike Feathers’ unit testing rules: it touches the file system. In these cases, I tend to side with Terry Pratchett’s Lu-Tze:
Rules are there to make you think before you break them.
I have never had problems caused by the occasional unit test that hits the file system, so I don’t worry too much especially if the alternative is lots more code. On the other hand, this test is writing to the ~/Documents directory, which is too messy. Let’s trick that out.
class TestTwitterAccount < Test::Unit::TestCase
def self.trick_account_plist_path
@@account_plist_path||= Tempfile.new("accountPlistPath").path
end
class OSX::TwitterAccount
def accountPlistPath
TestTwitterAccount.trick_account_plist_path
end
end
.....
end
Now the accountPlistPath method is not at all under test; let’s fix that by aliasing the original and showing that it saves to the user documents directory.
class OSX::TwitterAccount
objc_alias_method :original_accountPlistPath, :accountPlistPath
def accountPlistPath
TestTwitterAccount.trick_account_plist_path
end
end
......
def test_account_plist_is_saved_to_user_document_directory
assert_equal File.expand_path("~/Documents/account.plist"),
@testee.original_accountPlistPath
end
There is another test to write: let’s make sure we can handle having no previously saved preferences file.
def test_username_and_password_are_initially_nil_if_not_previously_saved
File.delete TestTwitterAccount.trick_account_plist_path
testee = OSX::TwitterAccount.alloc
testee.load
assert_equal nil, testee.username
assert_equal nil, testee.password
end
The alloc and load is looking a bit cumbersome. Let’s add a static factory method.
def setup
FileUtils.rm_f TestTwitterAccount.trick_account_plist_path
@testee = OSX::TwitterAccount.loadFromPlist
end
and
+(id)loadFromPlist{
TwitterAccount *result = [[TwitterAccount alloc]autorelease];
[result load];
return result;
}
Now let’s tidy up the test a bit.
require File.dirname(__FILE__) + '/test_helper'
require 'tempfile'
require 'fileutils'
require "TwitterAccount.bundle"
OSX::ns_import :TwitterAccount
class OSX::TwitterAccount
objc_alias_method :original_accountPlistPath, :accountPlistPath
def accountPlistPath
TestTwitterAccount.trick_account_plist_path
end
end
class TestTwitterAccount < Test::Unit::TestCase
def self.trick_account_plist_path
@@account_plist_path||= Tempfile.new("accountPlistPath").path
end
def setup
FileUtils.rm_f TestTwitterAccount.trick_account_plist_path
@testee = OSX::TwitterAccount.loadFromPlist
end
def test_account_plist_is_saved_to_user_document_directory
assert_equal File.expand_path("~/Documents/account.plist"),
@testee.original_accountPlistPath
end
def test_twitter_account_has_username_and_password
@testee.username = 'marvin'
@testee.password = 'tellnot'
assert_equal "marvin", @testee.username
assert_equal "tellnot", @testee.password
end
def test_account_details_may_be_saved_and_reloaded
@testee.username = 'victoria'
@testee.password = 'secret'
@testee.save
reloaded = OSX::TwitterAccount.alloc
reloaded.load
assert_equal "victoria", reloaded.username
assert_equal "secret", reloaded.password
end
def test_username_and_password_are_initially_nil_if_not_previously_saved
assert_equal nil, @testee.username
assert_equal nil, @testee.password
end
end
That’s good enough for now. I’m beginning to wonder whether the persistence logic should be in the same place as the domain logic: for now I think it should; it’s under test so we can refactor with confidence should things get more complex later.
Coming soon:
- unit testing sending a tweet to Twitter
- stretching Rbiphonetest to test interaction between our presentation code and UIKit classes.
- some Rbiphonetest gotchas
Voice recognition

I see from Gruber that there’s another voice recognition app out for the iPhone. Why? Do people actually want or use this stuff? I mean apart from playing around or edge-cases such as when driving (where, incidentally, hands free operation is still very dangerous).
Developing voice-recognition systems is a symptom of over exposure to Star Trek at a vulnerable age. Voice commands to computers works well on film and TV for dramatic reasons. It’s the same reason TV characters read letters out loud, and on-screen dialogue boxes use HUGE fonts. A dramatic on-screen device does not make for a good user interface design. There are three reasons why voice-recognition input is a poor choice for computing, particularly mobile-computing:
- Everyone can hear you.
- There are perfectly good alternatives, like keyboards and touchscreens.
- EVERYONE IN THE DAMN TRAIN CARRIAGE CAN HEAR YOU. And if the train’s delayed by snow they’re going to get annoyed, and possibly gang-up and kill you. It will be like the Orient Express all over again, except without Peter Ustinov.
I leave you with a link to this comic, on the subject, from Joy of Tech. You’ve been warned.