iOS と Mac OS X が同じ WiFi 下に入っていれば、自由にデータをやり取りすることができる Bonjour。AirPlay、AirPrint もこのテクノロジーに基づいているそうです。
あ、もちろん iOS 同士でも可能です。僕は普段から OS X アプリ と iOS アプリ両方作っているので二つの OS を横断した記事を書いてみることにしました。
実用的なアプリも作れそうですが、インタラクティブでエンタメ的なもの、オモシロイベントとかオモシロ展示とか作れたりできるんじゃないかなとも思います。
AdventCalendar で「入門的なこと書きます」と宣言してしまいましたがややニッチな内容になってしまいました。入門的なことは自分のブログに書きました。
##材料
Bonjour で使うクラスは NSNetService
, NSNetServiceBrowser
, NSInputStream
, NSOutputStream
などです。
##今回できるもの
iOS アプリから Mac OS X アプリに NSString
の変数を NSData
で送ってみて、データを受信した OS X アプリはソレを表示して、変数の中身が違っていたら別の表示するというサンプルを作成してみます。
##送信側の iOS アプリプログラミング
まずはデータを送信する iOS アプリ側です。
Xcode で Single View Application を作成し、「BonjouriOS」とかの名前で作成します。
今回は ARC を使ってみます。
###ヘッダー
Interface Builder でボタンを 5 個ぐらい作成して適当なボタン文字をつけて ViewController.h に Outlet を作成して、Action も設定しておきます。
さらに NSStreamDelegate
, NSNetServiceDelegate
, NSNetServiceBrowserDelegate
などのデリゲートプロトコルも設定しておきます。
#import <UIKit/UIKit.h>
@interface ViewController : UIViewController <NSStreamDelegate, NSNetServiceDelegate, NSNetServiceBrowserDelegate>
@property (strong, nonatomic) IBOutlet UIButton *button0;
@property (strong, nonatomic) IBOutlet UIButton *button1;
@property (strong, nonatomic) IBOutlet UIButton *button2;
@property (strong, nonatomic) IBOutlet UIButton *button3;
@property (strong, nonatomic) IBOutlet UIButton *button4;
- (IBAction)buttonWasTapped:(id)sender;
@end
とりあえず古き良き(全然リアルタイムじゃないけど・・・)昭和のギャグでも送信してみましょう。
###実装側
実装側ではホスト(接続ID)を探して、見つけたら呼び出される delegate を実装して NSNetService
を作成します。
#pragma mark ---
#pragma mark Bonjour Implementation
NSNetServiceBrowser* browser_;
NSNetService *service_;
NSInputStream *iStream_;
NSOutputStream *oStream_;
- (void)searchNetService
{
browser_ = [[NSNetServiceBrowser alloc] init];
browser_.delegate = self;
[browser_ searchForServicesOfType:@"_test._tcp" inDomain:@""];
}
-(void)netServiceBrowser:(NSNetServiceBrowser *)aNetServiceBrowser didFindService:(NSNetService *)aNetService moreComing:(BOOL)moreComing
{
NSLog(@"%@", [aNetServiceBrowser description]);
NSLog(@"%@", [aNetService description]);
service_ = [[NSNetService alloc] initWithDomain:[aNetService domain] type:[aNetService type] name:[aNetService name]];
if (service_)
{
// connected
service_.delegate = self;
[service_ resolveWithTimeout:5.0f];
}
else
{
NSLog(@"connect failed.");
}
}
さらに NSNetService
の delegate を実装してアウトプット用の NSStream
を作成します。
- (void)netServiceDidResolveAddress:(NSNetService *)sender
{
NSLog(@"%@", [sender description]);
NSInputStream *iStream = nil;
NSOutputStream *oStream = nil;
if (![sender getInputStream:&iStream outputStream:&oStream])
{
NSLog(@"cannot get streams");
return;
}
oStream_ = oStream;
if (oStream_)
{
[oStream_ open];
[oStream_ scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
}
ボタンなどを押したら NSStream
のインスタンスを使って送信します。
- (void) sendData:(NSString *)sendString
{
if (oStream_)
{
NSData *data = [sendString dataUsingEncoding:NSUTF8StringEncoding];
[oStream_ write:[data bytes] maxLength:[data length]];
}
}
以上が iOS 側です。
##受信側の Mac OS X アプリプログラミング
データを受信する OS X 側です。
Cocoa Application で「BonjourOSX」みたいな名前で新規プロジェクトします。Interface Builder でテキストフィールドを一つ作成します。NSNetServiceDelegate
デリゲートプロトコルも設定します。
###ヘッダー
#import <Cocoa/Cocoa.h>
@interface AppDelegate : NSObject <NSApplicationDelegate, NSNetServiceDelegate>
@property (assign) IBOutlet NSWindow *window;
@property (weak) IBOutlet NSTextField *textfield;
@end
###実装側
[NSNetService publish]
でホストを作成して、delegate を実装して NSFileHandle
のインスタンスを生成します。接続が来たら [NSFileHandle acceptConnectionInBackgroundAndNotify]
で接続許可を出してやります。
#define PORT_NUMBER 8888
NSSocketPort* socket_;
NSNetService *service_;
NSFileHandle *socketHandle_;
NSFileHandle* readHandle_;
#pragma mark ---
#pragma mark Bonjour Implementation
- (void)publishNetService
{
socket_ = [[NSSocketPort alloc] initWithTCPPort:PORT_NUMBER];
if (socket_)
{
service_ = [[NSNetService alloc] initWithDomain:@"" type:@"_test._tcp" name:@"Hello Bonjour" port:PORT_NUMBER];
if (service_)
{
service_.delegate = self;
[service_ scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
[service_ publish];
}
else
{
NSLog(@"invalid NSNetSevice");
}
}
else
{
NSLog(@"invalid NSSocketPort");
}
}
- (void)netServiceDidPublish:(NSNetService *)sender
{
NSLog(@"%@", [sender description]);
socketHandle_ = [[NSFileHandle alloc] initWithFileDescriptor:[socket_ socket] closeOnDealloc:YES];
if (socketHandle_)
{
NSLog(@"has sockethandle");
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(acceptConnect:) name:NSFileHandleConnectionAcceptedNotification object:socketHandle_];
[socketHandle_ acceptConnectionInBackgroundAndNotify];
}
}
許可が完了すればあとはデータを待つだけです。
- (void)acceptConnect:(NSNotification *)aNotification
{
NSLog(@"%@", [aNotification description]);
readHandle_ = [[aNotification userInfo] objectForKey:NSFileHandleNotificationFileHandleItem];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(recieveData:) name:NSFileHandleDataAvailableNotification object:readHandle_];
[readHandle_ waitForDataInBackgroundAndNotify];
}
データが来たら OS X 上の画面のビューに何かしてみましょう。
- (void)recieveData:(NSNotification *)aNotification
{
[self.window orderFront:self];
NSData *data = [readHandle_ availableData];
NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"%@", string);
[self.textfield setStringValue:string];
[readHandle_ waitForDataInBackgroundAndNotify];
}
最後に applicationWillTerminate
などのときに removeObserver
しておきましょう。
-(void)applicationWillTerminate:(NSNotification *)notification
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
##実行してみる
OS X アプリ立ち上げといて、iOS アプリを開いてボタンをぽちぽち。
師匠方のありがたいギャグが表示されます。
###他のサンプル
ちょっと昔に作ったビデオですが、これもこれと同じ方式で作成されたデモを撮影しています。
WiFi Based Data Transportation
##まとめ
みんな Bonjour してくまぼんじゅーーーる“(`(エ)´)ノ彡☆ !!