Fork me on GitHub

サンプルコード

https://github.com/sonsongithub/HandoffSample

ネイティブアプリケーション同士のHandoff - Xcodeの設定

ここまでで,NSUserActivityクラスのwebpageURLを使ったSafariへの簡単なURLの受け渡しだけを実装した. 次に,ネイティブアプリケーション同士のHandoffを実装してみる. 送信側は特別なことをする必要はないが,受信側はInfo.plistで処理するHandoffの識別子を明示する必要がある. Info.plistにNSUserActivityTypesキーでArrayを追加する. その配列にHandoffの識別子を逆ドメイン名フォーマットで追加する.

image

これでアプリケーションは,Handoffの受け取りが可能となる.

OSXの場合

OSXの場合もiOSと同様にInfo.plistに識別子を追加する. さらに,OSXであってもアプリケーションに署名をしなければ,Handoffを利用できない. OSXは設定しておけば,未署名のバイナリも実行できてしまうので注意を要する. 署名は,”Mac App 配布用証明書(Mac App Store)”, “Mac インストーラ配布用証明書(Mac App Store)”, “Developer ID アプリケーション証明書(Mac アプリケーション)”, “Developer ID インストーラ証明書 (Mac アプリケーション)”のどれでもいいようだ. しかしながら,筆者は,Developer ID アプリケーション証明書(Mac アプリケーション)による署名しかテストしていない.

SafariからネイティブアプリケーションをHandoffで起動する場合

このためには,JSONファイルへの署名等他に必要が作業が多い. 後日別エントリとして詳しく解説する.

送信側のコード

以後,特にiOSベースで解説する. 本質的なコードや挙動に関する議論は,OSXもiOSも変わらないはずである.

@interface ViewController () {
    NSUserActivity *_activity;
}
@end

@implementation ViewController
            
- (void)viewDidLoad {
    [super viewDidLoad];
    _activity = [[NSUserActivity alloc] initWithActivityType:@"com.sonson.HandoffSample"];
    _activity.title = @"Browsing";
    _activity.userInfo = @{
                            @"Key" : @"information from the other device"
                            @"URL" : @"http://www.apple.com"
                        };
    [_activity becomeCurrent];
}

@end

NSUserActivityのuserInfoにNSDictionaryで渡したい情報をコピーすればよい. いたって簡単だ.

受信側のコード

受け取り側のコードは,UIApplicationDelegate上で実装する.

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

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

- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity 
                                                   restorationHandler:(void(^)(NSArray *restorableObjects))restorationHandler {
    NSLog(@"application:continueUserActivity:restorationHandler:");
    restorationHandler(@[]);
    return YES;
}

まず,application:willContinueUserActivityWithType:は,Handoffの識別子を受け取り,処理を行うかを判定する. 状況が悪かったりするときは,ここでNOを返して,Handoffを実行しないようにすればいいだろう. application:didFailToContinueUserActivityWithType:error:は,,Handoffを受け取ったが処理に失敗した場合に呼び出される. このメソッドがどういったときに呼ばれるかわかりにくい(それこそベータ版の時はこのメソッドしか呼ばれないことが多々あったが).

application:continueUserActivity:restorationHandler:

最後のapplication:continueUserActivity:restorationHandler:で,具体的にHandoff経由で渡されたデータ等に基づいて,ビューの再構成を実行する. 引数で渡されるNSUserActivityオブジェクトにHandoffに関する情報が保持される. ややこしいのが,最後のrestorationHandlerだ. iOS8以降,UIResponderクラスにrestoreUserActivityState:メソッドが追加された. このメソッドは,Handoffによる状態復元を実行するためにある. restorationHandlerメソッドの引数にUIResponderのサブクラスのオブジェクトの配列を渡すと,その順番で,restoreUserActivityState:メソッドが配列の各オブジェクト毎にコールされる.

iPadのメールアプリのような2ペインのビュー構成だった場合,以下のような状態復元になる.

- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity 
                                                   restorationHandler:(void(^)(NSArray *restorableObjects))restorationHandler {
    // get or create view controller objects.
    id viewControllerA = nil;       // left pain
    id viewControllerB = nil;       // right pain
    
    // start to restore
    restorationHandler(@[viewControllerA, viewControllerB]);
    return YES;
}

@implementation ViewControllerAClass
- (void)restoreUserActivityState:(NSUserActivity *)activity {
    // restore left pain view using NSUserActivity object
}
@end

@implementation ViewControllerBClass
- (void)restoreUserActivityState:(NSUserActivity *)activity {
    // restore right pain view using NSUserActivity object
}
@end

すでに公開中のアプリケーションであれば,少なからず状態復元のコードは実装されていると考えるので,既存のコードがあるのなら,その復旧コードをapplication:continueUserActivity:restorationHandler:から呼び出せばいいだろう. 既存のコードでは対応できない復元項目があるのなら,このHandoffの仕組みを利用することも検討してもいいだろう.

2tch ネイティブアプリケーション同士のHandoffの例

webpageURL vs ネイティブアプリケーション同士のHandoff

ネイティブアプリケーション同士のHandoffは,受信側,送信側ともにアプリケーションがデバイスにインストールされていなければ,当然動作しない. そういうときのために,webpageURLuserInfoを両方入れておけばよい.

@interface ViewController () {
    NSUserActivity *_activity;
}
@end

@implementation ViewController
            
- (void)viewDidLoad {
    [super viewDidLoad];
    _activity = [[NSUserActivity alloc] initWithActivityType:@"com.sonson.HandoffSample"];
    _activity.webpageURL = [NSURL URLWithString:@"http://hoge.com/handoffSupport.html"];
    _activity.title = @"Browsing";
    _activity.userInfo = @{
                            @"Key" : @"information from the other device"
                            @"URL" : @"http://www.apple.com"
                        };
    [_activity becomeCurrent];
}

@end

そうすると,Handoffの仕組みでアプリケーションがインストールされていない場合は,SafariにwebpageURLが渡される. このとき,webpageURLにHandoffのサポートページやアプリケーションのダウンロードやインストールのページへのURLを渡したりすればユーザにスムーズにHandoffの仕組みを解説できる. ブラウザベースのアプリもネイティブアプリもあるサービスならば,ネイティブアプリ向けの情報をuserInfoに保管しておき,ブラウザ用のURLをwebpageURLに渡せばいいだろう.