前回の続き

前回は位置情報サービス許可のAlert表示のタイミングを説明しました。今回は位置情報サービスのオン・オフ状態に応じて、アプリ内でどのように処理し、UIに反映すると良いかを考察したいと思います。

注意すべきこと

まず注意すべきことを列挙します。
※ 以下、位置情報サービスがオフのときというのは「端末でオフ」「アプリ単位でオフ」両方を指します。

位置情報サービス許可の確認Alertは表示されることもあれば、されないこともある

位置情報サービス許可の確認Alertは、startUpdatingLocationを呼んだ時に、表示されることもあれば、表示されないこともあります。インストール後、最初にstartUpdatingLocationを呼び出した時は必ず表示されるようですが、それ以外のケースでは自分は特定しきれませんでした。いずせにせよ、表示されない場合は、プログラム側でユーザーに位置情報サービスがオフだということを伝える必要があるでしょう。

位置情報サービスの設定画面をプログラムから開くことはできない

表示されたりされなかったりするのが嫌であれば、独自のAlertを表示するという対処もできます。しかしアプリから設定画面を開くことはセキュリティの理由からできないので、メッセージでうまく伝える必要があります。

Twitter公式アプリ(5.8.2)は位置情報サービスがオフのときは、常に以下のようなAlertを表示します。

20

ボタンを押しても自動的に設定画面を開くことはできませんが、この説明ならユーザーにも伝わるでしょう。

ユーザーはいつでも位置情報サービスのオン・オフを切り替えられる

アプリを起動したままの状態で、設定から位置情報サービスのオン・オフを切り替えた時のことも考慮したUIにすべきです。一度、位置情報サービスをオフと判定したら、アプリを再起動するまで位置情報取得ボタンが押せなくなるといった挙動は避けるべきです。

startUpdatingLocationを呼んでも、エラーのdelegateが呼ばれないことがある

通常、位置情報サービスがオフのときに、startUpdatingLocationを呼ぶと位置情報取得に失敗し、エラーのdelegateが呼ばれます。ただし、以下の手順を踏むと呼ばれないようです。

  • 設定で端末の位置情報サービスをオンした状態でアプリを起動
  • 設定で端末の位置情報サービスをオフにする
  • startUpdatingLocationを呼ぶ
  • エラーのdelegateが呼ばれない

よって位置情報取得できない時の処理を、エラーのdelegateに集約してしまうのも良くないと思います。

ユーザビリティを損なわないプログラミングの例

個人的には以下の方法が良いのではないかと思います。

  • startUpdatingLocationを呼ぶ前にauthorizationStatusで、アプリの位置情報サービスの許可状態をチェック
  • kCLAuthorizationStatusNotDetermined(未確認)、kCLAuthorizationStatusAuthorized(許可済み)であれば、startUpdatingLocationを呼ぶ
    • このときkCLAuthorizationStatusNotDeterminedなら、アプリの位置情報取得を許可をするかどうかのAlertが表示される
  • kCLAuthorizationStatusDenied、kCLAuthorizationStatusRestrictedであれば、独自のAlertで設定から位置情報サービスをオンすることをうながす

locationServicesEnabledは、端末で位置情報サービスがオンかオフかを判定するもので、端末でオンならアプリがオフの場合もYESが返ってきます。アプリで位置情報取得できるかどうかの判定に不向きなので、authorizationStatusを使うようにしています。

kCLAuthorizationStatusNotDeterminedの時に、startUpdatingLocationを呼ぶようにしておけば、インストール直後ならアプリの位置情報サービス許可の確認Alertが表示されます。

11

やはり、このAlertは表示される条件分岐にしておくべきでしょう。

kCLAuthorizationStatusDenied、kCLAuthorizationStatusRestricted(NotDetermined、Authorizedでないとき)は、独自のAlertを表示します。

21

※ Twitter公式アプリを参考にしています。

ただし上記の条件分岐だと、インストール直後、端末の位置情報サービスがオフだった時、startUpdatingLocationを呼ばれることはありません。その時は、kCLAuthorizationStatusDeniedだからです。つまり以下のAlertは表示されるタイミングがありません。独自のAlertが表示されます。

10-1

このAlertの設定ボタンで設定画面へ移動できて親切なので、このAlertも表示したいのですが、判定が難しいので諦めることにします。

サンプル

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

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

31

Get Locationを押した時に位置情報を取得してマップにピンを立てます。Get Locationを押す度に位置情報を取得を試み、成功・失敗問わず完了したら位置情報取得を停止します。

Get Locationボタンを押すと、条件によって以下のAlertが表示されます。

インストール直後で、端末の位置情報サービスがオンのとき。

11

端末の位置情報サービスがオフの時、もしくは、一度上記Alertで許可しないを選んだ時。

21

位置情報取得ができたときは、現在位置に移動してピンが立ちます。

32

位置情報取得に失敗した時は、Alertを表示します。

33

STGetLocation2ViewControllerクラス

Get Locationボタンを押した時の処理です。

- (void)didTapGetLocationButton
{
    //
    // NotDetermined、Authorized以外(つまりDenied、Restricted)の時は、
    // 設定画面で位置情報サービスをオンすることをうながすAlertを表示する
    //
    CLAuthorizationStatus status = [CLLocationManager authorizationStatus];
    if (!((status == kCLAuthorizationStatusNotDetermined) ||
          (status == kCLAuthorizationStatusAuthorized))) {
        [[[UIAlertView alloc] initWithTitle:nil
                                    message:NSLocalizedString(@"Alert Location Service Disabled", nil)
                                   delegate:nil
                          cancelButtonTitle:nil
                            otherButtonTitles:@"OK", nil] show];
        return;
    }

    //
    // NotDeterminedの時は、startUpdatingLocationがアプリが位置情報サービス許可するかどうかのAlertを表示
    //
    [_locationManager startUpdatingLocation];
    
    _getLocationButton.enabled = NO;

    // startUpdatingLocationを呼んだ時の日時をメンバ変数に保存
    _startUpdatingLocationAt = [NSDate date];
    
    NSLog(@"Start updating location. timestamp:%@", [[NSDate date] description]);
}

位置情報を取得できた時のdelegateメソッド

- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations
{
    CLLocation *recentLocation = locations.lastObject;
    if (recentLocation.timestamp.timeIntervalSince1970 < _startUpdatingLocationAt.timeIntervalSince1970) {
        // 今回のstartUpdatingLocationによる位置情報で無い時は無視
        return;
    }

    // 位置情報取得を停止
    [_locationManager stopUpdatingLocation];
    
    _getLocationButton.enabled = YES;

    //
    // マップにピンを立てる
    //
    [_mapView setCenterCoordinate:recentLocation.coordinate animated:YES];
    
    MKPointAnnotation *mapAnnotation = [[MKPointAnnotation alloc] init];
    mapAnnotation.coordinate = recentLocation.coordinate;
    [_mapView removeAnnotations:_mapView.annotations];
    [_mapView addAnnotation:mapAnnotation];
    
    NSLog(@"Updated location:%f %f timestamp:%@", recentLocation.coordinate.latitude, recentLocation.coordinate.longitude, recentLocation.timestamp.description);
}

ここで注意すべきことは、locationManager:didUpdateLocationsは、今回startUpdatingLocationを押した時のものではなく、以前に取得した古い位置情報について呼ばれることがあることです。詳細はよくわからないですが、古い位置情報がキャッシュされていて、startUpdatingLocationを呼ぶと、locationManager:didUpdateLocationsに渡ってくるようです。このことを考慮せずにプログラミングしてしまうと、本当に今いる位置をアプリ上に反映できなくなる可能性があります。

よって上記のように、_startUpdatingLocationAtと取得した位置情報の日時を比較して、古ければ無視するようにしています。

位置情報取得に失敗した時はAlertを表示します。

- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error
{
    [_locationManager stopUpdatingLocation];
    
    _getLocationButton.enabled = YES;
    
    [[[UIAlertView alloc] initWithTitle:nil
                                message:NSLocalizedString(@"Failed to get your location.", nil)
                               delegate:nil
                      cancelButtonTitle:nil
                        otherButtonTitles:@"OK", nil] show];
}

その4へ続く