x-callback-url を使ったアプリ間連携実装のサンプル

# はじめに

x-callback-url を用いた別アプリ呼び出し処理の一例です。

流れや概要についてはcookpad様の丁寧な解説があります。
必読。

[iOSアプリ間連携の実装に x-callback-url を使う](http://techlife.cookpad.com/entry/2014/11/14/143841)

以下は主に「これを実装してみた！」内容です。

# 概要

別アプリを起動する場合、2パターンあると思います。
　a. 別アプリを呼び、すぐに制御を元のアプリに戻す
　b. 別アプリを呼び、元アプリに制御は戻らずそのまま操作できる

今回は前者のケース。

UUIDを生成し保持しているアプリAがあるとして、
アプリBからアプリAを起動しUUIDを貰ってきます。

# 工夫・調査した点

### 一瞬見える、呼び出されたアプリの画面表示

元のアプリに制御を返す時に一瞬だけ、呼び出したアプリの画面が見えてしまいます。
それが嫌だったので隠すようにしました。

### 元アプリに制御を戻す際の遅さへの対応

こちらを参考にさせて頂きました。
[iOS7で起動時にsafariへ飛ばすときにはGCDを使う](http://qiita.com/kaz3439/items/85ea924f091c123ea7d0)

### カスタムURLスキームでの呼び出し時の画像(保留)
カスタムURLスキームでのアプリ呼び出し時の画像は変更可能でした。
[カスタムURLスキームでのアプリケーション切替時の画像の設定](http://safx-dev.blogspot.jp/2010/10/url.html)

しかしiOS8.3ではできないようです。
調査中です。

# 呼び出す側アプリ実装

[InterAppCommunication](https://github.com/tapsandswipes/InterAppCommunication)
こちらをプロジェクトに追加しておきます。

あとは自分のURLスキームの登録をしておきます。
[カスタムURLスキームを利用する (3)](http://develop.calmscape.net/dev/232/)

例では自分のURLスキームは「jp.co.sample」としました。
呼び出される側のURLスキームは「jp.co.sample-parent」としました。

以下、主な部分の実装です。

```objc:AppDelegate.m

//
//  AppDelegate.m
//  CustomSchemeSample
//
//  Created by Satoshi Hattori on 2015/06/24.
//  Copyright (c) 2015年 Satoshi Hattori. All rights reserved.
//

#import "AppDelegate.h"
#import "IACManager.h"

#define SELF_SCHEME_NAME @"jp.co.sample"

@interface AppDelegate ()

@end

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    [IACManager sharedManager].callbackURLScheme = SELF_SCHEME_NAME;
    
    return YES;
}

- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation
{
    return [[IACManager sharedManager] handleOpenURL:url];
}

@end

```

```objc:ViewController.m

//
//  ViewController.m
//  CustomSchemeSample
//
//  Created by Satoshi Hattori on 2015/06/24.
//  Copyright (c) 2015年 Satoshi Hattori. All rights reserved.
//

#import "ViewController.h"
#import "IACClient.h"

#define PARENT_SCHEME_NAME @"jp.co.sample-parent"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];

    // 取得処理を呼ぶためだけのボタン
    UIButton *btn = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    btn.frame = CGRectMake(100, 100, 120, 35);
    [btn setTitle:@"UUID取得" forState:UIControlStateNormal];
    [btn addTarget:self action:@selector(uuidBtn_touchUpInside:) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:btn];
}

- (void)uuidBtn_touchUpInside:(id)sender
{
    NSString *parentScheme = [NSString stringWithFormat:@"%@://", PARENT_SCHEME_NAME];
    
    if (![[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:parentScheme]]) {
        return;
    }
    
    IACClient *client = [IACClient clientWithURLScheme:PARENT_SCHEME_NAME];
    
    // コールバックありの呼び出し
    [client performAction:@"getUuid" parameters:nil onSuccess:^(NSDictionary *params) {
        
        NSString *uuid = params[@"uuid"];
        
        UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"UUID" message:uuid delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
        [alert show];
        
    } onFailure:^(NSError *error) {
        
        UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Error" message:[error localizedDescription] delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
        [alert show];
        
    }];
}

@end

```


# 呼び出される側アプリ実装

こちらのプロジェクトでもInterAppCommunicationを追加しておきます。

自分のURLスキームの登録を忘れずに。
こちらのURLスキームは「jp.co.sample-parent」としました。

```objc:AppDelegate.m

//
//  AppDelegate.m
//  CustomSchemeSampleParent
//
//  Created by GCSMAC01 on 2015/06/24.
//  Copyright (c) 2015年 Satoshi Hattori. All rights reserved.
//

#import "AppDelegate.h"

#import "IACManager.h"
#import "MyIACLogic.h"
#import "SchemeBackgroundView.h"

#define PARENT_SCHEME_NAME @"jp.co.sample-parent"

@interface AppDelegate ()
{
    MyIACLogic *_myIACLogic;
}

@end

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    _myIACLogic = [[MyIACLogic alloc] init];
    
    [IACManager sharedManager].callbackURLScheme = PARENT_SCHEME_NAME;
    [IACManager sharedManager].delegate = _myIACLogic;
    
    return YES;
}

- (void)applicationDidEnterBackground:(UIApplication *)application
{
    // アプリがBackgroundに移行するタイミングで一時的な背景Viewの除去を行っています
    [SchemeBackgroundView dismiss];
}

#pragma mark - Custom URL Scheme

- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation
{
    return [[IACManager sharedManager] handleOpenURL:url];
}

@end

```

IACDelegateを実装したロジッククラス。

```objc:MyIACLogic.h
//
//  MyIACLogic.h
//  CustomSchemeSampleParent
//
//  Created by Satoshi Hattori on 2015/06/24.
//  Copyright (c) 2015年 Satoshi Hattori. All rights reserved.
//

#import <Foundation/Foundation.h>
#import "IACManager.h"

@interface MyIACLogic : NSObject <IACDelegate>

@end

```

```objc:MyIACLogic.m
//
//  MyIACLogic.m
//  CustomSchemeSampleParent
//
//  Created by Satoshi Hattori on 2015/06/24.
//  Copyright (c) 2015年 Satoshi Hattori. All rights reserved.
//

#import "MyIACLogic.h"
#import "SchemeBackgroundView.h"

@implementation MyIACLogic

- (BOOL)supportsIACAction:(NSString *)action
{
    NSArray *supportedActions = @[@"getUuid"];
    return [supportedActions containsObject:action];
}

- (void)performIACAction:(NSString *)action
              parameters:(NSDictionary *)parameters
               onSuccess:(IACSuccessBlock)success
               onFailure:(IACFailureBlock)failure
{
    if ([action isEqualToString:@"getUuid"]) {

        if (success) {
            
            // 一時的な背景Viewを最善面に追加
            [SchemeBackgroundView show];

            // ここがロジック部分
            // ここではダミーUuid値を生成
            NSString *uuid = @"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";

            // 数秒(3〜5秒)待たされる現象があったため対応としてdispatch_asyncで実行
            dispatch_async(dispatch_get_main_queue(), ^{
                success(@{@"uuid":uuid}, NO);
            });
            
        }
    }
    else {
        // その他のアクションの場合
    }
    
}

@end

```

一時的な背景View。
最前面にaddSubviewしています。

```objc:SchemeBackgroundView.h

//
//  SchemeBackgroundView.h
//  CustomSchemeSampleParent
//
//  Created by GCSMAC01 on 2015/06/24.
//  Copyright (c) 2015年 Satoshi Hattori. All rights reserved.
//

#import <UIKit/UIKit.h>

@interface SchemeBackgroundView : UIView

+ (void)show;
+ (void)dismiss;

@end

```

```objc:SchemeBackgroundView.m

//
//  SchemeBackgroundView.m
//  CustomSchemeSampleParent
//
//  Created by Satoshi Hattori on 2015/06/24.
//  Copyright (c) 2015年 Satoshi Hattori. All rights reserved.
//

#import "SchemeBackgroundView.h"

@implementation SchemeBackgroundView

+ (SchemeBackgroundView *)sharedView
{
    static dispatch_once_t once;
    static SchemeBackgroundView *sharedView;
    dispatch_once(&once, ^ { sharedView = [[self alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; });

    sharedView.backgroundColor = [UIColor blackColor];

    return sharedView;
}

+ (void)show
{
    NSEnumerator *frontToBackWindows = [[[UIApplication sharedApplication] windows] reverseObjectEnumerator];
    
    for (UIWindow *window in frontToBackWindows) {
        if (window.windowLevel == UIWindowLevelNormal) {
            [window addSubview:[SchemeBackgroundView sharedView]];
            break;
        }
    }
}

+ (void)dismiss
{
    NSEnumerator *frontToBackWindows = [[[UIApplication sharedApplication] windows] reverseObjectEnumerator];
    
    for (UIWindow *window in frontToBackWindows) {
        if (window.windowLevel == UIWindowLevelNormal) {
            for (UIView *view in window.subviews) {
                if ([view isKindOfClass:[SchemeBackgroundView class]]) {
                    [view removeFromSuperview];
                }
            }
            break;
        }
    }
}

@end

```


# 気を付ける箇所

・カスタムURLスキームには「_」アンダースコアは使用できない。(なんという罠！)

・InterAppCommunication使用時、localizedAppNameがnilだとエラー。
　→　info.plistに「Bundle display name」を追加し設定しましょう。


# おわりに

実際にやってみると罠や気になる挙動が出てきますよね。
それに対応することでじわじわと貯まるノウハウ。

細かい点でまだ課題ありですが
同じ事をやろうとした方の参考になりましたら幸いです。

# 関連

[x-callback-url](http://x-callback-url.com/)
[InterAppCommunication](https://github.com/tapsandswipes/InterAppCommunication)