なら@はてなブログ

福岡で働くスマートフォンエンジニア(おっさん)のブログ。更新頻度がとにかく低いのが悩み。

iOS4のマルチタスク対応とUIImagePickerController

とあるアプリの開発で以下のような機能を作ってみました。

  • カメラのプレビューに対してARで情報を表示(位置はGPSで特定)
  • アプリが起動していなくても、対象が近くにくると通知

ということで、ARについてはUIImagePickerControllerをモーダル表示してOverlayViewをかぶせるだけのよくあるパターンです。

バックグラウンドタスクについては、AppDelegateで定義されているapplicationDidEnterBackground、applicationWillEnterForegroundを使用。裏でGPSからの通知を受け続けて(もちろんバックグラウンド中は精度を落としてます。バッテリー食いそうだし)、対象が近くに来たらUILocalNotificationで通知。

おいらiPhone4しか持ってないのでそれで動作確認してたら、特に問題なく動いたのね。マルチタスク使う時点でiOS4以降対応だし。

で、だいたい作ってから、iOS4にしたiPhone3GSでも確認しようとしたら、バックグラウンドからの起動時にブラックアウト。UIImagePickerControllerインスタンス自体は生きてるんだけど、「デバイスの準備ができていません」という状態。困った。ただ、AR表示用にかぶせてあるOverlayViewは普通に表示できてるので、本当にデバイスから情報がとれてないだけなんだなあ、と。

ソースはだいたいこんな感じ。
見る人が見ればわかると思いますが、iPhoneARKitのソースをまんま使わせてもらってます。

HogeViewController.m
- (void) init {
    // 必要な部分だけ抜粋
#if !TARGET_IPHONE_SIMULATOR
    // カメラ作成
    self.cameraController = [[UIImagePickerController alloc] init];
    self.cameraController.sourceType = UIImagePickerControllerSourceTypeCamera;

    // デフォルトだとプレビュー画面が全画面にならないので、少し拡大。
    // これ、frameとかtransformで座標移動させてもなんかうまくいかないのね。
    self.cameraController.cameraViewTransform = CGAffineTransformScale(self.cameraController.cameraViewTransform, 1.08, 1.08);

    self.cameraController.showsCameraControls = NO;
    self.cameraController.navigationBarHidden = YES;
    self.cameraController.delegate = self;
#endif
}

- (void)viewDidAppear:(BOOL)animated {
    // UIImagePickerControllerをモーダル表示する場合、イベントとしては
    // viewDidAppearでやらないとうまいこと表示されないみたい。
#if !TARGET_IPHONE_SIMULATOR
    [self.cameraController setCameraOverlayView:ar_overlayView];
    [self presentModalViewController:self.cameraController animated:NO];
    [ar_overlayView setFrame:self.cameraController.view.bounds];
#endif
}
HogeAppDelegate.h
@interface HogeAppDelegate : NSObject <UIApplicationDelegate> {
    UIWindow *window;

    // AR表示用ViewController
    HogeViewController *mViewController;
    // バックグラウンドタスク管理用
    UIBackgroundTaskIdentifier mBgTask;
}
HogeAppDelegate.m
- (void)applicationDidFinishLaunching:(UIApplication *)application {
    // ここでAR表示データとか作ってるけど省略
}

- (void)applicationDidEnterBackground:(UIApplication *)application {
    // このへんでバックグラウンドに移るときの処理を記述
    
    // バックグラウンドタスクを登録
    mBgTask = [application beginBackgroundTaskWithExpirationHandler:^{[application endBackgroundTask:mBgTask];}];
}

- (void)applicationWillEnterForeground:(UIApplication *)application {
    // バックグラウンドから復帰したときの処理を記述
}

で、前述の通り、これだとiPhone4ではいい感じに動いてくれるのですが、iPhone3GSiOS4)だとバックグラウンドから復帰してもカメラのプレビューが表示されません。AR情報だけが虚しく表示されることに。

その後、applicationWillEnterForegroundでUIImagePickerControllerをいったんリリースしてから再作成してみましたが、それでもうまく行かず。
そもそも、モーダル表示を止めた時点で、HogeViewControllerのviewWillAppearとviewDidAppearが走って面倒なことに。まあ、モーダルが消えたら自前Viewが表示されるので、そのへんのイベントが呼ばれるのは正しい仕様だと思いますが。

で、そもそもバックグラウンドに移行するときに裏で動かないようにしたら(applicationDidEnterBackground、applicationWillEnterForegroundを実装しない)、iPhone3GSでもカメラは表示される。もちろん、裏で動かないのでGPS情報取得してプッシュ通知はできないわけですが。
なので、iPhone3GSの場合はプッシュ通知は捨てて、最低限の機能を提供しようという結論に。根本的な解決策が思いつかなかったので悔しいところではありますが、お仕事でやっている以上スケジュールというものがありまして。しかも、そもそもかなり無茶なスケジュールでやってるので。

で、プリプロセッサで機種がわかればいいなー? と思ったんですが、自前でマクロ組まないとダメそうなので、今回は通常のソース内で対応。
判別方法はid:KishikawaKatsumiさんのダイアリーに書いてありました。多謝。
d:id:KishikawaKatsumi:20090909:1252508346

というわけで、AppDelegateをこんな感じに。
適当に書いてますが、実際はimport宣言の場所とかはきちんと書かないとね。

HogeAppDelegate.m
#include <sys/sysctl.h>

- (NSString *)platform {
    size_t size;
    sysctlbyname("hw.machine", NULL, &size, NULL, 0);
    char *machine = malloc(size);
    sysctlbyname("hw.machine", machine, &size, NULL, 0);
    /*
     Possible values:
     "iPhone1,1" = iPhone 1G
     "iPhone1,2" = iPhone 3G
     "iPhone2,1" = iPhone 3GS
     "iPhone3,1" = iPhone 4
     "iPod1,1"   = iPod touch 1G
     "iPod2,1"   = iPod touch 2G
     */
    NSString *platform = [NSString stringWithCString:machine encoding: NSUTF8StringEncoding];
  
    free(machine);
    return platform;
}

BOOL mChecked;
BOOL isIPhone4;

- (void)applicationDidEnterBackground:(UIApplication *)application {
    if (!mChecked) {
        isIPhone4 = [[self platform] isEqualToString:@"iPhone3,1"];
        mChecked = YES;
    }
    if (isIPhone4) {
        // このへんでバックグラウンドに移るときの処理を記述
    
        // バックグラウンドタスクを登録
        mBgTask = [application beginBackgroundTaskWithExpirationHandler:^{[application endBackgroundTask:mBgTask];}];
    }
}

・・・と、書きましたが実機インストールは会社でしかできないので(さすがに個人所有のMacにCertificate入れるわけにはいかず)、動作確認してないんですけどね。


追記

だめでした。
どうもコンソール見てるとメモリ足りないときに呼ばれるメソッド(名前忘れた)が呼ばれてるので、そっち関係かなあ?
うーん?
原因と対策がわかったらまた追記します。