前回の続き

前回はGet Locationボタンを押したら位置情報を取得し、完了したら位置情報取得を停止するサンプルでした。今回は連続的に位置情報を取得し続けるナビゲーションのようなサンプルです。

サンプル: https://github.com/stack3/CLLocationManagerSamples

サンプルを起動してNavigate Locationを選択すると以下の画面が表示されます。

現在位置にピンが立ち、移動する度にピンの位置が更新されます。

01

位置情報サービスがオフの時は、その旨が表示されます。

02

位置情報サービスがオンだが、屋内などで位置情報取得できなかったときは、以下のように表示されます。

03

ポイントは、

  • 位置情報取得開始は画面を開いた時に始まる。つまりボタンを押すなどユーザーのアクションを伴わない
  • エラー表示をAlertで行わない

ことです。

連続して位置情報を取得を試みている時は、成功、失敗が交互に混ざることがあり、その度にAlertで「位置情報取得に失敗しました」と表示されるとユーザビリティが下がります。よって、このようにLabelで表示しています。

位置情報サービスがオフの時は以下の様な挙動をします。

  • startUpdatingLocationを呼んで位置情報取得開始
  • エラーdelegateが来る時と来ない時がある
  • 設定を開いて位置情報サービスをオンに切り替える
  • 位置情報取得成功のdelegateが来る

ナビゲーション系の処理の場合は、位置情報サービスがオフであれstartUpdatingLocationは呼んでおくと良いと思います。オンにしたときに位置情報取得のdelegateがくるようになるからです。

位置情報サービスがオフの時はAlert表示しても良いかもしれません。ただタイミングが難しいです。位置情報取得エラーのdelegateが来る時と来ない時があるからです。つまり、位置情報取得開始前とエラーdelegateの両方でオフかどうかチェックし、エラー表示する必要があります。表示がAlertだと2回表示される可能性があります。

STNavigateLocationViewControllerクラス

viewDidLoadでSubviewやLocationManagerの初期化

- (void)viewDidLoad
{
    [super viewDidLoad];

    // MapViewの最初の中心座標(適当)
    _mapView.region = MKCoordinateRegionMake(CLLocationCoordinate2DMake(37.332331, -122.031219), MKCoordinateSpanMake(0.01, 0.01));
    //
    // エラーを表示するLabel
    //
    _errorLabel.textColor = [UIColor whiteColor];
    // 半透明にする
    _errorLabel.backgroundColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:0.5];
    _errorLabel.text = nil;
    // 最初は非表示に
    _errorLabel.hidden = YES;
    //
    // CLLocationManagerの生成
    //
    _locationManager = [[CLLocationManager alloc] init];
    _locationManager.delegate = self;
}

viewDidAppear(viewが表示された時)で、位置情報取得開始。また、このときに位置情報サービスのオン・オフなどをチェックして、必要ならエラー表示を行う。

- (void)viewDidAppear:(BOOL)animated
{
    // 位置情報取得開始
    [_locationManager startUpdatingLocation];
    _startUpdatingLocationAt = [NSDate date];
    // 必要ならエラー表示を行う
    [self updateErrorLabelWithIgnoreAuthorized:YES];
    // アプリケーションがバックグラウンドから復帰し、アクティブになった時の処理
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(handleApplicationDidBecomeActive:)
                                                 name:UIApplicationDidBecomeActiveNotification object:nil];
}

エラーラベルを更新するメソッド。随所で呼ばれます。

- (void)updateErrorLabelWithIgnoreAuthorized:(BOOL)isIgnoreAuthorized
{
    CLAuthorizationStatus status = [CLLocationManager authorizationStatus];

    // isIgnoreAuthorizedがYESなら、statusがAuthorizedの時、無視する
    if (isIgnoreAuthorized && (status == kCLAuthorizationStatusAuthorized)) {
        _errorLabel.text = nil;
        _errorLabel.hidden = YES;
        return;
    }

    // statusに応じたエラー表示    
    if (status == kCLAuthorizationStatusAuthorized) {
        // 屋内などで位置情報取得できなかった場合
        _errorLabel.text = NSLocalizedString(@"Failed to get your location.", nil);
    } else {
        // 位置情報サービスがオフの場合
        _errorLabel.text = NSLocalizedString(@"Alert Location Service Disabled", nil);
    }
    _errorLabel.hidden = NO;
    //
    // マップ上のピンを取り除く
    //
    if (_mapAnnotation) {
        [_mapView removeAnnotation:_mapAnnotation];
        _mapAnnotation = nil;
    }
}

viewWillDisappear(viewが非表示になる直前)で、位置情報取得を停止し、Notificationの監視をやめます。

- (void)viewWillDisappear:(BOOL)animated
{
    // 位置情報取得停止
    [_locationManager stopUpdatingLocation];
    _startUpdatingLocationAt = nil;
    // Notificationの監視をやめる
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

UIApplicationDidBecomeActiveNotificationを受け取るのは、設定を開いて位置情報サービスのオン・オフを切り替えて、アプリに戻ってきた時のためです。位置情報サービスの状態をチェックしエラー表示を更新します。

- (void)handleApplicationDidBecomeActive:(NSNotification *)notitication
{
    [self updateErrorLabelWithIgnoreAuthorized:YES];
}

位置情報取得成功のdelegate。マップの中心位置とピンの位置を更新します。

- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations
{
    CLLocation *recentLocation = locations.lastObject;
    if (recentLocation.timestamp.timeIntervalSince1970 < _startUpdatingLocationAt.timeIntervalSince1970) {
        // 古い位置情報は無視
        return;
    }
    // 取得した位置を中心にする
    [_mapView setCenterCoordinate:recentLocation.coordinate animated:YES];
    //
    // Annotation(ピンの元)が生成されていなければ生成しMapViewに追加
    //
    if (_mapAnnotation == nil) {
        _mapAnnotation = [[MKPointAnnotation alloc] init];
        [_mapView addAnnotation:_mapAnnotation];
    }
    // 現在位置を設定
    _mapAnnotation.coordinate = recentLocation.coordinate;
    //
    // エラーを非表示にする
    // 
    _errorLabel.text = nil;
    _errorLabel.hidden = YES;

    NSLog(@"Updated location:%f %f timestamp:%@", recentLocation.coordinate.latitude, recentLocation.coordinate.longitude, recentLocation.timestamp.description);
}

位置情報取得に失敗した時、エラー表示を更新。ただし、ここはstatusがAuthorizedでも呼ばれうるので(屋内で位置情報取得できないなど)、IgnoreAuthorized引数はNOにします。

- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error
{
    [self updateErrorLabelWithIgnoreAuthorized:NO];
}

この方法が絶対的に正しいとは限りませんが、

  • ユーザーがなぜ位置情報取得できないのかわからない
  • 位置情報サービスをオンにしたが、アプリを再起動、もしくは画面を戻って再度開かないと、位置情報取得しない

といったことは回避できる内容になっていると思います。

このサンプルを起動したまま、

  • 位置情報サービスのオン・オフを切り替える
  • シミュレータなら「メニュー > デバッグ > 位置 > なし」を選択して、位置情報取得失敗をシミュレートする

などを試して動きを見てみると参考になると思います。