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

iOS と Mac OS X が同じ WiFi 下に入っていれば、自由にデータをやり取りすることができる **Bonjour**。AirPlay、AirPrint もこのテクノロジーに基づいているそうです。

あ、もちろん iOS 同士でも可能です。僕は普段から [OS X アプリ](http://itunes.apple.com/jp/app/image-bind/id530343153?mt=12 "Image Bind") と [iOS アプリ](http://itunes.apple.com/jp/app/icolors-color-swatches/id414588543?mt=8 "iColors - Color Swatches")両方作っているので二つの OS を横断した記事を書いてみることにしました。

実用的なアプリも作れそうですが、インタラクティブでエンタメ的なもの、オモシロイベントとかオモシロ展示とか作れたりできるんじゃないかなとも思います。

AdventCalendar で「入門的なこと書きます」と宣言してしまいましたがややニッチな内容になってしまいました。入門的なことは[自分のブログ](http://feb19.jp/blog/archives/000248.php "Hello iOS SDK and Objective-C: UIKit/Foundation Frameworks")に書きました。

##材料

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

```objectivec: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](http://feb19.jp/qiita/bonjour_ios.png 'Bonjour iOS')

###実装側

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

```objectivec: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` を作成します。

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

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

###ヘッダー

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

```objectivec: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];
    }
}
```

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

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

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

```objectivec:AppDelegate.m
-(void)applicationWillTerminate:(NSNotification *)notification
{
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}
```

##実行してみる

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

![Bonjour OS X](http://feb19.jp/qiita/bonjour_osx.png 'Bonjour OS X')

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

###他のサンプル

ちょっと昔に作ったビデオですが、これもこれと同じ方式で作成されたデモを撮影しています。
[WiFi Based Data Transportation](https://vimeo.com/35311490 "WiFi Based Data Transportation")

##まとめ

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