話題の記事

[iOS][iBeacon] iOS 7.1 からアプリを起動していなくても領域観測できるようになったので、さまざまなバックグラウンド処理を試してみた

2014.05.23

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

iOS 7.1

ちょっと前の話になりますが、2014年3月11日 に iOS 7.1 がリリースされました。このアップデートで iBeacon が改善されている件が開発者の間で話題になっていました。

特に重要なのがアプリを起動していなくても領域観測できるようになったという点です。iBeacon の機能で一番問題視されていたところだったので、これはかなり嬉しい機能改善ですよね。ということで今回はバックグラウンドでさまざまな処理を試してみました。

領域観測を試してみる

まずは領域観測です。iBeacon の実装方法は iOS 7.0 のときと変わりありませんので、サクッと実装します。今回は AppDelegate で実装したいと思います。また、Beacon の情報を送信するアプリはこちらの記事を参考に実装してください。

BERAppDelegate.h

#import "BERAppDelegate.h"

@interface BERAppDelegate () <CLLocationManagerDelegate>

@property (strong, nonatomic) NSUUID *proximityUUID;
@property (strong, nonatomic) CLLocationManager *manager;
@property (strong, nonatomic) CLBeaconRegion *region;

@end

@implementation BERAppDelegate

#pragma mark - UIApplicationDelegate methods

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    if ([CLLocationManager isMonitoringAvailableForClass:[CLBeaconRegion class]]) {
        // CLLocationManagerの生成とデリゲートの設定
        self.manager = [CLLocationManager new];
        self.manager.delegate = self;
        
        // 生成したUUIDからNSUUIDを作成
        NSString *uuid = @"1E21BCE0-7655-4647-B492-A3F8DE2F9A02";
        self.proximityUUID = [[NSUUID alloc] initWithUUIDString:uuid];
        
        // CLBeaconRegionを作成
        self.region = [[CLBeaconRegion alloc]
                       initWithProximityUUID:self.proximityUUID
                       identifier:@"jp.classmethod.testregion"];
        self.region.notifyOnEntry = YES;
        self.region.notifyOnExit = YES;
        self.region.notifyEntryStateOnDisplay = NO;
        
        // 領域観測を開始
        [self.manager startMonitoringForRegion:self.region];
    }
    return YES;
}

#pragma mark - CLLocationManagerDelegate methods

// Beaconに入ったときに呼ばれる
- (void)locationManager:(CLLocationManager *)manager
         didEnterRegion:(CLRegion *)region
{
    NSLog(@"%@", NSStringFromSelector(_cmd));
    [self sendNotification:@"didEnterRegion"];
}

// Beaconから出たときに呼ばれる
- (void)locationManager:(CLLocationManager *)manager
          didExitRegion:(CLRegion *)region
{
    NSLog(@"%@", NSStringFromSelector(_cmd));
    [self sendNotification:@"didExitRegion"];
}

#pragma mark - Other methods

- (void)sendNotification:(NSString*)message
{
    // 通知を作成する
    UILocalNotification *notification = [[UILocalNotification alloc] init];
    
    notification.fireDate = [NSDate dateWithTimeInterval:10 sinceDate:[NSDate new]];
    notification.timeZone = [NSTimeZone defaultTimeZone];
    notification.alertBody = message;
    notification.alertAction = @"Open";
    notification.soundName = UILocalNotificationDefaultSoundName;
    
    // 通知を登録する
    [[UIApplication sharedApplication] scheduleLocalNotification:notification];
}

@end

それでは実行してみます。まずアプリを起動するとユーザーの承認が求められるので、許可します。

ibeacon-bgt01

次に、アプリをタスクから消して終了します。

ibeacon-bgt02

このあとスリープさせ、 Beacon の送信側の端末を起動して領域内に入ると…

ibeacon-bgt03

無事に通知されました!同様に、領域内から外れた場合も通知されます。

ibeacon-bgt04

距離観測(レンジング)と組み合わせてクーポン情報を通知する

Beacon 領域内に入るとアプリがバックグラウンドで起動し、CLLocationManagerDelegatelocationManager:didEnterRegion: が呼び出されます。このようにシステムからアプリが呼び出される際、10秒前後の処理時間が与えられます。この間であれば何らかの処理を行うことができるというわけです。

この時間を利用して、領域観測でどの Beacon が近くに居るか判別し、クーポン情報を通知するしくみを実装してみたいと思います。

locationManager:didEnterRegion: の中で CLLocationManagerstartRangingBeaconsInRegion: を呼び出し、領域観測を開始します。locationManager:didRangeBeacons:inRegion: が約1秒おきくらいに呼び出されるようになるので、1回呼び出されたら Major 値や Minor 値を見てどのお店の情報を出せば良いか判別して通知を出します。最後に stopRangingBeaconsInRegion: を呼び出し、距離観測を終わらせます。そうしておかないと数回通知されてしまいます。

// Beaconに入ったときに呼ばれる
- (void)locationManager:(CLLocationManager *)manager
         didEnterRegion:(CLRegion *)region
{
    NSLog(@"%@", NSStringFromSelector(_cmd));
    [self sendNotification:@"didEnterRegion"];
    [self.manager startRangingBeaconsInRegion:self.region];
}

// Beaconから出たときに呼ばれる
- (void)locationManager:(CLLocationManager *)manager
          didExitRegion:(CLRegion *)region
{
    NSLog(@"%@", NSStringFromSelector(_cmd));
    [self sendNotification:@"didExitRegion"];
    [self.manager stopRangingBeaconsInRegion:self.region];
}

// 距離観測が更新されるたびに呼ばれる
- (void)locationManager:(CLLocationManager *)manager
        didRangeBeacons:(NSArray *)beacons
               inRegion:(CLBeaconRegion *)region
{
    NSLog(@"%@", NSStringFromSelector(_cmd));
    
    // 有効なBeaconを1つ取り出す
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"proximity != %d", CLProximityUnknown];
    NSArray *validBeacons = [beacons filteredArrayUsingPredicate:predicate];
    CLBeacon *beacon = validBeacons.firstObject;
    if (beacon.major == 0) {
        [self sendNotification:@"クラメソ電機でクーポン配布中!"];
    }
    // 距離観測を終了する
    [self.manager stopRangingBeaconsInRegion:self.region];
}

これで実装完了です。さきほどと同じ手順で実行してみると、バックグラウンドで動作していない状態でクーポン情報が通知されます。

ibeacon-bgt05

補足ですが、通知の判定処理で proximity で距離を判定し、近くに来たら通知するというしくみは実現不可能なので注意してください。これは端末を持っているユーザーの行動(すぐに近くに行くとか、立ち止まるとか)で大きく変わってしまうためです。領域内に入って数秒間はほとんど CLProximityFarCLProximityUnknown になると思います。

バックグラウンドでどのくらい処理が行えるのか調べてみる

おまけとして、バックグラウンドでアプリが起動した場合、どのくらい処理が行えるのか調べてみました。先ほどは10秒程度と書きましたが、実際のところどのくらいできるでしょうか。

普通に処理を行った場合

まずは普通に処理を行った場合を検証してみます。locationManager:didEnterRegion: が呼び出された時刻と locationManager:didRangeBeacons:inRegion: が呼び出された時刻の差を通知するだけです。

// Beaconに入ったときに呼ばれる
- (void)locationManager:(CLLocationManager *)manager
         didEnterRegion:(CLRegion *)region
{
    NSLog(@"%@", NSStringFromSelector(_cmd));
    [self sendNotification:@"didEnterRegion"];
    
    // 開始時刻を記録
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    [defaults setObject:[NSDate date] forKey:@"startDate"];
    [defaults synchronize];
    
    [self.manager startRangingBeaconsInRegion:self.region];
}

// 距離観測が更新されるたびに呼ばれる
- (void)locationManager:(CLLocationManager *)manager
        didRangeBeacons:(NSArray *)beacons
               inRegion:(CLBeaconRegion *)region
{
    NSLog(@"%@", NSStringFromSelector(_cmd));
    
    NSTimeInterval interval = [[NSDate date] timeIntervalSinceDate:[defaults objectForKey:@"startDate"]];
    [self sendNotification:[NSString stringWithFormat:@"処理時間 = %.3f秒",interval]];
}

これまでと同じ手順で実行してみます。結果は約9秒まで通知できました。10秒前後ということで、短くなることもあるようです。

ibeacon-bgt06

また、locationManager:didExitRegion: が呼ばれる直前にも1回呼び出されることがわかりました。

ibeacon-bgt07

Background Task を利用した場合

次に Background Task を利用してみます。Background Task は iOS 4 で登場した、その名の通りバックグラウンドで少し長い処理を行うために用意されている機能です。iOS 7 では 約180秒間(約3分間)の処理を行うことができるようになります。Background Task を利用するには、UIApplicationbeginBackgroundTaskWithExpirationHandler: メソッドを呼びます。

// Beaconに入ったときに呼ばれる
- (void)locationManager:(CLLocationManager *)manager
         didEnterRegion:(CLRegion *)region
{
    NSLog(@"%@", NSStringFromSelector(_cmd));
    [self sendNotification:@"didEnterRegion"];
    
    // 開始時刻を記録
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    [defaults setObject:[NSDate date] forKey:@"startDate"];
    [defaults synchronize];
    
    // Background Task を実行
    [self runBackgroundTask];
}

// Background Task を実行する
-(void)runBackgroundTask {
    UIApplication *application = [UIApplication sharedApplication];
    
    __block UIBackgroundTaskIdentifier bgTask = [application beginBackgroundTaskWithExpirationHandler:^{
        dispatch_async(dispatch_get_main_queue(), ^{
            // 既に実行済みであれば終了する
            if (bgTask != UIBackgroundTaskInvalid) {
                [application endBackgroundTask:bgTask];
                bgTask = UIBackgroundTaskInvalid;
            }
        });
    }];
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // 距離観測を開始する
        [self.manager startRangingBeaconsInRegion:self.region];
    });
}

// 距離観測が更新されるたびに呼ばれる
- (void)locationManager:(CLLocationManager *)manager
        didRangeBeacons:(NSArray *)beacons
               inRegion:(CLBeaconRegion *)region
{
    NSLog(@"%@", NSStringFromSelector(_cmd));
    
    NSTimeInterval interval = [[NSDate date] timeIntervalSinceDate:[defaults objectForKey:@"startDate"]];
    [self sendNotification:[NSString stringWithFormat:@"処理時間 = %.3f秒",interval]];
}

実行してみます。結果は約186秒まで通知することができました。3分も時間があればある程度の距離観測はできるかも知れませんね(何とも言えませんが)。また、locationManager:didExitRegion: など他のイベントをトリガにすることもできます。

ibeacon-bgt08

Background Task を組み合わせれば、一度だけ距離観測して major 値などを取得し、Webサーバーにパラメータで送信するなどという処理も可能です。

まとめ

Reject に注意!

iBeacon の話とは少しそれますが、関連する話題を共有したいと思います。以前 新刊Push というアプリをリリースしたのですが、App Review Guideline の 5.3 の 「最初にユーザーの承認を得ないでプッシュ通知を送るアプリはリジェクト」と 5.6 の「プッシュ通知を広告、宣伝、いかなる種類のマーケティングにも使う事は出来ない」いう項目に引っかかったことがあります。

bookpush

App Store で見る

このアプリは「Background Fetch を使って本の情報を取ってきて、新刊があったら Local Notification で通知する」という機能を持っていて、これが「ユーザーの確認を取らずに Push Notification している」と解釈されてしまいました。結局、APNS の登録処理だけ(実際には使っていませんが、ユーザーの承認アラートを出すため)実装し、通知内容を当たり障りもなさそうなメッセージに変えて再申請したらレビューを通過することができました。

上記の経験は iBeacon の話ではないのですが、iBeacon でも同じことが言えるのではないかと思っています。実際、機能的には APNS を使っているわけではないので Push Notification ではないのですが、ユーザー側から考えればどう見ても Push Notification ですよね。iBeacon を使ってバックグラウンドで通知するようなアプリは App Review Guideline に抵触する可能性があるので、注意してください。