Make sure your Spotify application is up and running for this to work properly.
[iOS] Reading BT keyboard presses
There are times when you’ll want to read key presses coming from a Bluetooth keyboard in your iOS application. This post addresses a technique I’ve come up with to do this
This method uses NSNotificationCenter and a UITextField to do this. A thing to note is that if you don’t have a BT keyboard paired with your iDevice, you’ll get the virtual keyboard to appear. If paired, you won’t see the virtual keyboard, so there is no need to force-hide it or anything like that. If the keyboard becomes unpaired or powers off, you’ll see the virtual keyboard pop up from the bottom.
So we are going to read the single key presses coming into the iOS application (much like we do in Flash… however we don’t get a keyUp, keyDown, etc.)
Place a UITextField in your application via Xcode & declare it in your code (ie. IBOutlet UITextField *textField, etc.)… here is the little crust. This is NOT localized!
#pragma mark - Keyboard
- (void)handle_TextFieldTextChanged:(id)notification {
NSString *sValue = self.textField.text;
if([sValue isEqualToString:@"a"]){
NSLog(@"a was pressed");
}
//Avoid a loop for the change, happens even on setting to empty string
[self.textField resignFirstResponder];
self.textField.text = @"";
[self.textField becomeFirstResponder];
}
- (void)registerForTextFieldNotifications {
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter addObserver:self
selector:@selector (handle_TextFieldTextChanged:)
name:UITextFieldTextDidChangeNotification
object:self.textField];
}
#pragma mark - View lifecycle
- (void)viewDidLoad {
[super viewDidLoad];
self.textField.hidden = YES;
[self.textField becomeFirstResponder];
[self registerForTextFieldNotifications];
}
You can hide the control offscreen. I chose to simply hide it altogether since I used Xcode to place the control and didn’t do it programmatically.
I haven’t done anything in regards to localization and this technique only reads a character at a time without any notion of keyDown and keyUp which would be nice to determine a press and hold using an NSTimer or something like that. The only solution I can think of at the moment would be for a custom keyboard that itself sent the correct character and then followed the release with a custom value used as a delimiter that you could respond to while ignoring it’s actual value. I’ll keep thinking about that bit.
Popularity: 2%
iOS: indexing local HTML files contents for search
Situation: You’re serving up a bunch of local HTML files in your iOS application as content in some fashion, and you’d like to be able to give the user the opportunity to be able to search that file, and all other possible files, for a keyword. They would be presented with some form of user interface to allow them to choose which page to jump to in the available pages.
I have seen many javascript examples around where you include a method in your HTML files, but that really only works for the HTML presently residing within your UIWebView control. You want the user to be able to search throughout all the files.
My quick solution does not use a .plist file (but it could). It also doesn’t take into account page titles, etc. All I am capturing are which keywords are associated with which URLs so that I can provide a starting point for this kind of search functionality.
So this is just a jumping off point, but it’s using NSLinguisticTagger to provide “nouns” to use as keywords. Since we’re using HTML I want to be able to strip out all of the tags easily. My local method currently looks like this:
- (NSString *)flattenHTML:(NSString *)html {
NSScanner *thescanner;
NSString *text = nil;
thescanner = [NSScanner scannerWithString:html];
while ([thescanner isAtEnd] == NO) {
// find start of tag
[thescanner scanUpToString:@"<" intoString:nil];
// find end of tag
[thescanner scanUpToString:@">" intoString:&text];
// replace the found tag with a space
//(you can filter multi-spaces out later if you wish)
html = [html stringByReplacingOccurrencesOfString:
[NSString stringWithFormat:@"%@>", text]
withString:@" "];
}
return [html stringByTrimmingCharactersInSet:
[NSCharacterSet whitespaceCharacterSet]];
}
Now in my indexing method, I deploy the guts. I am creating an NSLinguisticTagger so I can check for nouns, getting all the .html files in my application as an array, looping through the array getting the path for the file, all the contents of the file as a string, stripping out all the HTML tags from the string, finding the nouns, assigning mutableArrays for each noun, etc. It’s kind of slick.
- (void)indexAllHTML
{
NSLinguisticTagger *tagger = [[NSLinguisticTagger alloc]
initWithTagSchemes:[NSArray arrayWithObjects: NSLinguisticTagSchemeTokenType,
NSLinguisticTagSchemeLexicalClass,
NSLinguisticTagSchemeNameType,nil]
options:0];
//Parse through the HTML files.
NSMutableArray *index = [[NSMutableArray alloc] init];
myDictionary = [[NSMutableDictionary alloc] init];
NSArray *pages = [[NSBundle mainBundle] pathsForResourcesOfType:@".html" inDirectory:nil];
for(int i=0; i<[pages count]; i++){
NSString *path = [pages objectAtIndex:i];
NSString *pageData = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
NSArray *foo = [path componentsSeparatedByString:@"/"];
NSString *shortPath = [foo lastObject];
//We want all nouns for the pages to build an index from.
pageData = [self flattenHTML:pageData];
[tagger setString:pageData];
NSRange textRange = NSMakeRange(0, [pageData length]);
[tagger enumerateTagsInRange:textRange scheme:NSLinguisticTagSchemeLexicalClass options:0 usingBlock:^(NSString *tag, NSRange tokenRange, NSRange sentenceRange, BOOL *stop) {
if(tag == NSLinguisticTagNoun){
NSString *word = [pageData substringWithRange:tokenRange];
//Trim out stragglers we don't want to be indexed. Your milage will vary!
if(![word isEqualToString:@"reg"] && ![word isEqualToString:@"III"] && ![word isEqualToString:@"labeled"]
&& ![word isEqualToString:@"are"] && ![word isEqualToString:@"13"]){
//NSLog(@"%d. %@", i, [pageData substringWithRange:tokenRange]);
if([myDictionary objectForKey:word] == nil){
//key doesn't exist yet, create nsmutablearray for it.
NSMutableArray *anArray = [[NSMutableArray alloc] init];
[anArray addObject:shortPath];
[myDictionary setObject:anArray forKey:word];
} else {
//key already exists, add to the nsmutablearray in it.
NSMutableArray *arr = [myDictionary objectForKey:word];
//No repeats for same url. Why bother.
if(![arr containsObject:shortPath]){
[arr addObject:shortPath];
}
}
if(![index containsObject:word]){
[index addObject:word];
}
}
}
}];
}
[index sortUsingSelector:@selector(localizedCaseInsensitiveCompare:)];
//Some random logging so you get the picture.
NSLog(@"done finding nouns. Number nouns = %d", [index count]);
NSLog(@"1: %@, 20: %@", [index objectAtIndex:0], [index objectAtIndex:19]);
NSLog(@"power paths count: %d", [[myDictionary objectForKey:@"power"] count]);
NSMutableArray *temp = [myDictionary objectForKey:@"power"];
NSLog(@"first path: %@, 2: %@, 3: %@", [temp objectAtIndex:0], [temp objectAtIndex:1], [temp objectAtIndex:2]);
NSLog(@"\n");
NSEnumerator *enumerator = [myDictionary keyEnumerator];
NSString *myKey;
while(myKey = [enumerator nextObject]){
NSLog(@"> %@ :: %@\n", myKey, [myDictionary objectForKey:myKey]);
}
}
And there you have it. All your HTML pages indexed (right now by short path only).
Titles for the pages aren’t retained in this code. I’ve run this on about 80 HTML files as a test and it all logs in around 236ms on an iPad2. Not too shabby.
The .plist could be faster in the end, but for now there isn’t enough of a hit to speed or memory to warrant storage like that.
Enjoy.
Popularity: 3%
GameKit: Connecting different applications
For the past few days I was struggling with GameKit – notably getting it working properly. I was told in a few places that applications connecting over GameKit Bluetooth needed to have the same bundle ID. Which on the surface makes sense… normally it’s used to connect the same game in a multi-player type of situation. However, that was wrong and I needed two different applications to connect over something and I had chosen GameKit.
Normally when using a GKPeerPickerController the session is a default – and uses the bundle ID. Hence if you had two different applications trying to connect they wouldn’t show up in the picker. However using a delegate method (GKPeerPickerControllerDelegate) you’re able to supply your own sessionID. That means that two different applications CAN see one another and create a connection. The didConnectPeer is ignored and the sessionID you provide will be used.
- (GKSession *)peerPickerController:(GKPeerPickerController *)picker sessionForConnectionType:(GKPeerPickerConnectionType)type
{
if(currentSession == nil){
currentSession = [[GKSession alloc] initWithSessionID:@"Foo" displayName:@"Hey There" sessionMode:GKSessionModePeer];
currentSession.delegate = self;
}
return currentSession;
}
Now when the apps choose to connect, the string “Hey There” will be presented in the respective pickers and the applications will connect with the sessionID “Foo”. You’re free to have them communicate back and forth… which is awesome because I was blocked until I found this. I almost opted for Bonjour and socket streaming. I may still, but at least I have BT working between the two applications.
A woot moment for moi
Popularity: 4%
GameKit giving me a BT status of not available?
I’ve been using GameKit to connect an app together with different iOS devices. Suddenly know when I run the app and I choose to connect by bringing up the GKPeerPickerController, I get this in the console:
- BTM: attaching to BTServer
- <<< Session >>> +[GKBluetoothSupport _determineBluetoothStatus]: BT not available – try again later.
- BTM: posting notification BluetoothAvailabilityChangedNotification
I have no idea why this is happening as BT is enabled on the iOS devices in question. This is using the latest XCode 4 which I was using before without any problems. The code is pretty simple…
-(IBAction) btnConnect:(id) sender {
picker = [[GKPeerPickerController alloc] init];
picker.delegate = self;
picker.connectionTypesMask = GKPeerPickerConnectionTypeNearby;
[connect setHidden:YES];
[disconnect setHidden:NO];
[picker show];
}
Since I am using the picker I am not declaring a session myself. It should just work, but I get the BT not available error. This is on various iOS devices. I have played with the compiler (which one is being used) to no avail. I am wondering what might cause this behavior because it’s blocking me pretty badly at the moment.
Any ideas?
Update (May 17, 2011 @8:41 AM EDT):
While I still get that strange status, what I did to see if it would still work was this: turn OFF Bluetooth on the iOS device, then ran the application, and of course when the picker is called up and BT is off, you’re prompted to enable BT. When I did that this would be able to connect…
- BTM: attaching to BTServer
- <<< Session >>> +[GKBluetoothSupport _determineBluetoothStatus]: BT not available – try again later.
- BTM: posting notification BluetoothAvailabilityChangedNotification
- BTM: received BT_LOCAL_DEVICE_POWER_STATE_CHANGED event
- BTM: posting notification BluetoothPowerChangedNotification
- BTM: Adding new device 0x192b60 Verizon iPhone 24:AB:81:E3:63:BB 0xf51c0016
- BTM: _btServiceEventCallback: service = 0×800 eventType = 0 event = 1 result = 0
- BTM: _btServiceEventCallback: service = 0×800 eventType = 0 event = 11 result = 0
- BTM: received BT_SERVICE_CONNECTION_RESULT event with 1 currently connected services
- BTM: posting notification BluetoothDeviceConnectSuccessNotification
- BTM: received BT_LOCAL_DEVICE_CONNECTION_STATUS_CHANGED event
- BTM: posting notification BluetoothConnectionStatusChangedNotification
This is at least a work-around even though I have no idea why it’s required all of the sudden.
Popularity: 5%
