Edited at
iOSDay 12

iOS と Mac OS X 間を Bonjour で通信してちょっと楽しげなこと

More than 5 years have passed since last update.

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 などのデリゲートプロトコルも設定しておきます。


ViewController.h

#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


とりあえず古き良き(全然リアルタイムじゃないけど・・・)昭和のギャグでも送信してみましょう。

Bonjour iOS IB


実装側

実装側ではホスト(接続ID)を探して、見つけたら呼び出される delegate を実装して NSNetService を作成します。


ViewController.m

#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 を作成します。


ViewController.m

- (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 のインスタンスを使って送信します。


ViewController.m

- (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 デリゲートプロトコルも設定します。


ヘッダー


AppDelegate.h

#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] で接続許可を出してやります。


AppDelegate.m

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


許可が完了すればあとはデータを待つだけです。


AppDelegate.m

- (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 上の画面のビューに何かしてみましょう。


AppDelegate.m

- (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 しておきましょう。


AppDelegate.m

-(void)applicationWillTerminate:(NSNotification *)notification

{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}


実行してみる

OS X アプリ立ち上げといて、iOS アプリを開いてボタンをぽちぽち。

Bonjour OS X

師匠方のありがたいギャグが表示されます。


他のサンプル

ちょっと昔に作ったビデオですが、これもこれと同じ方式で作成されたデモを撮影しています。

WiFi Based Data Transportation


まとめ

みんな Bonjour してくまぼんじゅーーーる“(`(エ)´)ノ彡☆ !!