Fork me on GitHub

サンプルコード

https://github.com/sonsongithub/HandoffSample

データの受け渡し

ネイティブアプリケーション同士でのHandoffでは,データを受け渡すことができる. 3種類の受け渡し方法があるが,通信方法やデータの種類に応じて,適切な方法を選ぶ必要がある. BluetoothとWiFiを利用して通信するが,やはり大量のデータを安易にHandoffでやり取りするのはよくないようだ.

userInfo

NSUserActivityオブジェクトのuserInfoにデータを引き渡し,通信することができる. NSUserActivityは,周囲のiOS/OSXデバイスへのブロードキャストに用いられることを考えると,大きなデータを割り当てるべきではないことは明白である. この方法は最も簡単なので使いがちだが,画像や音声などの大きなデータを受け渡すことは推奨されない. URLや数値,文字列程度にとどめておくべきだろう.

streamを使う

Handoffには,それを受け入れた後に,ストリームによる送受信の通信経路を提供する機能が備わっている. それを利用すれば容易にデータをやり取りすることが可能だ. ストリームはOSが自動的に確保してくれる. 送信側がストリームを使うことを明示し,受信側からストリームを開くことで,通信を開始できる. ストリームのためにNSInputStreamNSOutputStreamのインスタンスがOSから提供される.

まずは,送信側のコードを見てみる.

@interface ViewController () <NSUserActivityDelegate> {
    NSUserActivity *_activity;
    NSData *_dataToSend = nil;
}
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    _activity
        = [[NSUserActivity alloc] initWithActivityType:@"com.sonson.OSX.HandoffSample"];
    _activity.title = @"Browsing";
    _activity.supportsContinuationStreams = YES;
    _activity.delegate = self;
    _activity.userInfo = @{@"DataSize":@(_dataToSend.length)};
    [_activity becomeCurrent];
}

- (void)userActivity:(NSUserActivity *)userActivity 
    didReceiveInputStream:(NSInputStream *)inputStream
             outputStream:(NSOutputStream *)outputStream {

    NSInteger dataSize = dataToSend.length;
    NSInteger sendSize = 0;
    NSInteger packetSize = 128;
    
    [outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop]
                            forMode:NSDefaultRunLoopMode];
    [outputStream open];
    
    uint8_t *p = (uint8_t*)[_dataToSend bytes];
    
    while (1) {
        NSInteger bytesToSend
            = (dataSize - sendSize) > packetSize ? packetSize : (dataSize - sendSize);
        NSInteger result
            = [outputStream write:p + sendSize maxLength:bytesToSend];
        
        if (result < 0) {
            NSLog(@"Error - %ld", result);
            break;
        }
        
        sendSize += result;
        if (sendSize >= dataSize) {
            break;
        }
    }
    [_activity invalidate];
    [outputStream close];
    outputStream = nil;
}

- (void)userActivityWasContinued:(NSUserActivity *)userActivity {
}

- (void)userActivityWillSave:(NSUserActivity *)userActivity {
}

@end

まず,NSUserActivityクラスのsupportsContinuationStreamsYESにする. そして,NSUserActivitydelegateをセットし,デリゲートメソッド処理するように実装しよう. ストリームを扱うために,userActivity:didReceiveInputStream:outputStream:メソッドを実装する. userActivity:didReceiveInputStream:outputStream:メソッドは,受信側からストリームが開かれたタイミングで呼ばれる. 今回のサンプルは,送信側からNSOutputStreamを通じて,バイナリデータを送るものである.

@interface AppDelegate () {
}
@end

@implementation AppDelegate

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
    // Insert code here to initialize your application
}

- (void)applicationWillTerminate:(NSNotification *)aNotification {
    // Insert code here to tear down your application
}

- (BOOL)application:(NSApplication *)application
    willContinueUserActivityWithType:(NSString *)activityType {
    if ([activityType isEqualToString:@"com.sonson.OSX.HandoffSample"])
        return YES;
    return NO;
}

- (void)application:(NSApplication *)application 
    didFailToContinueUserActivityWithType:(NSString *)userActivityType
                                    error:(NSError *)error {
    NSLog(@"application:didFailToContinueUserActivityWithType:error:");
    NSLog(@"%@", error);
}

- (BOOL)application:(NSApplication *)application
    continueUserActivity:(NSUserActivity *)userActivity
      restorationHandler:(void(^)(NSArray *restorableObjects))restorationHandler {

    [userActivity getContinuationStreamsWithCompletionHandler:^(NSInputStream *inputStream, NSOutputStream *outputStream, NSError *error) {
        if (error == nil) {
            NSNumber *num = userActivity.userInfo[@"DataSize"];
            NSInteger dataSize = num.integerValue;
            
            [inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
            [inputStream open];
            
            NSInteger receivedBytes = 0;
            NSInteger length = 256;
            uint8_t buffer[length];
            
            NSMutableData *readData = [NSMutableData data];
            while (1) {
                NSInteger bytesRead = [inputStream read:buffer maxLength:length];
                if (bytesRead <= 0)
                    break;
                [readData appendBytes:buffer length:bytesRead];
                receivedBytes += bytesRead;
                if (receivedBytes >= dataSize)
                    break;
            }
            [inputStream close];
        }
        else {
            NSLog(@"%@", [error localizedDescription]);
        }
    }];
    restorationHandler(@[]);
    return YES;
}

@end

iCloudを使う

UIDocument/NSDocumentは,自動的にNSUserActivityを持つらしい. これを利用して,Handoffでドキュメントデータの識別子(URL?)を送り,それに基づきiCloudを通じて,ドキュメントを共有する仕組みが提供されている. 筆者は,まだこの機能をテストしていない・・・・.