2017年04月21日

[iOS] モーダル表示処理のバッドノウハウ

iOS担当のエモトです.何に使えばいいのか分からないですが,GPD Pocketに出資してみました.かつてネットブックが盛んだったころ,EeePCを始め数台のネットブックを買っては遊んだ思い出を繰り返すんだろうなと思います.

モーダル表示したViewControllerが閉じたときの判定は,いうまでもなくdismissしたときのcompletionブロックを使えば取得できます.
- (void)dismissViewControllerAnimated:(BOOL)flag
					   	 completion:(void (^)(void))completion
しかしながら,サードパーティーSDKなどでモーダルの表示と削除がそのSDK内部で行われている場合はどうしましょうか.一般的なものであれば,デリゲートメソッドなどが用意され,取得可能だと思われます.
- (void)hogehogeWillDismiss;
- (void)hogehogeDidDismiss;
しかしながら,
- (void)hogehogeDismiss;
という場合もあります.これだとdismissがwillなのかdidなのかよくわかりません.実際に開発している人にはよく分かると思いますが,willとdidの違いは,画面制御にて大きな問題になります.開発者はdidを期待して実装したら,実はwillだったとなれば,アプリの想定外の動作やクラッシュの原因になります.

私自身もこれに似た設計のSDKに遭遇したことがあり,提供元に確認したところ,そのSDKの仕様から判定できないと回答されました.今回は試行錯誤のもとでその問題を回避した方法を紹介します.しかしながら,根本的な良い方法ではないことであるとご認識ください.

方針として,モーダルの表示と削除はサードパーティーSDKが行なっているので,内部の現象を外部から観測することで解決を試みます.まず,モーダルで表示されたViewControllerは以下のように取得できるので,これを手掛かりとして探っています.
UIViewController *vc = self.presentedViewController;
これを監視すれば解決しそうです.以前に当ブログでも紹介したKVO(Key-Value Observing, キー値監視)を使えば良い気もしますが,残念ながらpresentedViewControllerは監視できないので使えません.やりたくはないですが,タイマーを回して,presentedViewControllerを監視します.

例として,HogeHogeSDKというサードパーティーSDKを仮定します.このSDKは,``show:``メソッドで指定したViewControllerを基底としてモダール表示するとします.また,デリゲートメソッドとして,``hogehogeDismiss``が用意され,SDKで表示したViewControllerがdismissされたときに呼ばれるとします.しかしながら,dismissが完了されたかどうかは用意されてません.
@interface HogeHogeSDK : NSObject
- (void)show:(UIViewController*)viewController;
@end

@protocol HogeHogeSDKDelegate <NSObject>
- (void)hogehogeDismiss;
@end
手順ですが,
1. モーダル表示されたら,そのpresentedViewControllerを別の変数に代入しておく
2. デリゲートを受け取ったら,presentedViewControllerに対して以下の判定を行う
- nilなら,完了メソッドを行う
- nilでなければ,nilになるまで待ったのち完了メソッドを行う
注意点として,presentedViewControllerで取得されるUIViewControllerは非公開のUIViewControllerを継承したクラスである場合が多く,何が表示されているかわからない(逆にクラス名を保持しておけば判定材料になる).モーダル表示はSDKだけはなく,自身のアプリで表示されたり,OSが表示するものもあるので,完了条件はそれぞれの環境に合わせて設定しなければなりません.

@interface ViewController ()

// サードパーティSDK
@property (nonatomic, retain) HogeHogeSDK *hogeHogeSDK;

// モーダルで表示されるViewController
@property (nonatomic, weak) UIViewController *tempViewController;

// モーダルで表示されるViewControllerのクラス
@property (nonatomic, retain) Class tempClass;

// タイマー関数の制御
@property (nonatomic, assign) NSTimeInterval progress;
@property (nonatomic, retain) NSTimer *timer;

@end

@implementation ViewController

#pragma mark - HogeHogeSDK

// サードパーティSDKで何かしらを表示させる
- (void)showHogeHogeSDK
{
    [self.hogeHogeSDK show:self];
    
    // モーダル表示がアニメーション遷移の場合に直後だと取得できないので,ディレイ処理を入れる
    dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));
    dispatch_after(delay, dispatch_get_main_queue(), ^(void){
        self.tempViewController = self.presentedViewController;
        self.tempClass = self.tempViewController.class;
    });

}

// サードパーティSDKの表示が終わったときに行う処理
- (void)compleHogeHogeSDK{
    // 別の画面に遷移など
}

#pragma mark - HogeHogeSDKDelegate

- (void)hogeHogeSDKDismiss:(NSString *)appID
{
	// モーダル表示がまだ残っているなら,タイマー処理を始まる.そうでなければ完了処理へ
    if (self.tempViewController) {
        [self startTimer];
    }else{
        [self compleHogeHogeSDK];
    }
}


#pragma mark モーダルを監視する

- (void)startTimer
{
    [self endTimer];
    self.progress = 0;
    self.timer = [NSTimer scheduledTimerWithTimeInterval:0.1
                                                  target:self
                                                selector:@selector(handleTimer:)
                                                userInfo:nil
                                                 repeats:true];
}

- (void)endTimer
{
    if (self.timer) {
        [self.timer invalidate];
        self.timer = nil;
        self.progress = 0;
    }
}


- (void)handleTimer:(NSTimer*)timer
{
    self.progress += timer.timeInterval;
    
    UIViewController *vc = self.presentedViewController;
    
    if (self.progress > 3) {
    // dismiss後にある一定以上たっても状態が変わらなかったら,強制的に終了させる
        [self endTimer];
        if (self.tempViewController) {
            [self.tempViewController dismissViewControllerAnimated:false
                                                             completion:^{
                                                                 [self compleHogeHogeSDK];
                                                             }];
        }else{
            [self compleHogeHogeSDK];
        }
    }else if (vc == nil
              || self.tempViewController == nil
              || (vc && [vc isKindOfClass:self.tempClass] == false)
              ) {
              /*
              以下のルールのどれかに当てはまれば完了メソッドを呼ぶ
              - 現在のモダールがnil
              - モーダル表示したときに代入したvcがnil
              - モーダルがあるが,SDKで設定したクラスではない
              */
        [self endTimer];
        [self compleHogeHogeSDK];
    }
    
}

@end
今回はこのような方法で問題を回避できましたが,決して良い方法ではありません.内部の現象をある仮定のもとで外部から観測しているだけなので,今回はたまたま上手くいっただけで,SDKの内部処理によっては失敗します.ぜひ,完了メソッドを実装されてないSDK提供者様は実装をお願いします.
posted by Seesaa京都スタッフ at 12:00| Comment(0) | iOS | このブログの読者になる | 更新情報をチェックする
この記事へのコメント
コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント: