今回は、iOSアプリで位置情報を取得するときに配慮しなくてはいけない点を自分なりにまとめてみようと思います。
#はじめに
私は今、現在地から飲食店を検索するアプリを開発しています。
※App Storeにリリースしているので良かったらダウンロードしてみて下さい!
ふっぴーくん
自分の現在地から飲食店を検索するアプリなので、もちろん、現在地を取得しなくてはいけません。だったら、現在地を取得して「はい、終わり!」というシンプルなものではありませんでした。
まずは、現在地を取得するために最低限必要な手順から書いていきたいと思います。
#手順
iOSアプリで位置情報を取得するためには、
1.プライバシー情報取得の許可
2.CoreLocationフレームワークをインポート、CLLocationManagerクラスを初期化
3.位置情報サービスを使用するために、ユーザーに許可をリクエスト
4.CLLocationManagerDelegateプロトコルを採用し、デリゲートメソッドを実装
おおまかには、こんな感じの手順で位置情報が取得できます。
では、順を追って説明していきます。
#1.プライバシー情報取得の許可
iOSアプリでは、プライバシーに関わる情報を取得する際に許可が必須
です。
位置情報もプライバシーに関わる情報なので、位置情報サービスを使用する前に許可を貰わないと使うことができません。
許可を得るには、info.plistにて使いますよ!という意思表示を示しましょう。
Key:Location When In Use Usage Description
Value:任意の文字列(例:このアプリは位置情報サービスを使います。)
#2.フレームワークをインポート、クラスを初期化
まずは、位置情報関連のサービスがアプリ内で使えるようにCoreLocationフレームワークをインポートします。
import CoreLocation
そして、CoreLocationフレームワークに含まれているCLLocationManagerクラスを初期化します。
このクラスは、位置情報の機能を管理
してくれています。
このクラスを、メンバプロパティ(クラス内で定義されるプロパティ)として
ViewControllerで宣言しインスタンスを初期化していきます。
import CoreLocation
final class ViewController: UIViewController {
// CLLocationManagerクラスのインスタンスlocationManagerをメンバプロパティとして宣言
var locationManager: CLLocationManager = {
// インスタンスを初期化
var locationManager = CLLocationManager()
// 取得精度の設定(1km以内の精度)
locationManager.desiredAccuracy = kCLLocationAccuracyKilometer
// 位置情報取得間隔(5m移動したら位置情報取得)
locationManager.distanceFilter = 5
return locationManager
}()
}
CLLocationManagerクラスには、位置情報取得の精度
や位置情報取得の間隔
など色々な設定を自分で調節できます。
宣言と同時に書いてあるdesiredAccuracy・distanceFilterプロパティの事ですね。
自分のアプリに合った設定をすることで、バッテリーが長持ちするというメリットがあります。
位置情報サービスはかなりの電力を使いますので、こういう所も配慮しなくてはいけませんね。
#3.ユーザーに許可をリクエスト
位置情報サービスを使用するときは、ユーザーに対して許可をリクエスト
しなくてはいけません。
今回は、アプリの使用中に許可をリクエストしたいのでCLLocationManagerクラスのメソッドrequestWhenInUseAuthorization()を使います。
import CoreLocation
final class ViewController: UIViewController {
// CLLocationManagerクラスのインスタンスlocationManagerをメンバプロパティとして宣言
private var locationManager: CLLocationManager = {
// インスタンスを初期化
var locationManager = CLLocationManager()
// 取得精度の設定(1km以内の精度)
locationManager.desiredAccuracy = kCLLocationAccuracyKilometer
// 位置情報取得間隔(5m移動したら位置情報取得)
locationManager.distanceFilter = 5
return locationManager
}()
override func viewDidLoad() {
super.viewDidLoad()
// ユーザーに許可をリクエスト
locationManager.requestWhenInUseAuthorization()
}
}
ここで一旦、ビルドしてみたいと思います。
そもそも端末の位置情報サービスがオフ
になっているとこんな感じのアラートが表示されます。
設定
を選択することで設定アプリに画面遷移しますので、タップして位置情報サービスをオン
にしてみましょう。
※ただし、アプリをインストール後の初回起動のみしか表示されません。
位置情報サービスをオン
にしてから再度、アプリを起動すると、こんな感じのアラートが表示されます。
このアラートは、requestWhenInUseAuthorization()
を呼ぶと表示されます。
1度だけ許可
やAppの使用中は許可
などの選択肢がありますね。
これは位置情報サービスの認証ステータス
でいくつかの種類があります。
認証ステータスは、CLAuthorizationStatusにまとめており、
アプリを初めて起動した時はnotDetermined
になっています。
なので現状、ビルドしたアプリはnotDetermined
状態という事ですね。
分かりやすいように認証ステータスをまとめてみました↓
認証ステータス | 意味 |
---|---|
authorizedAlways | 常に許可 |
authorizedWhenInUse | Appの使用中は許可(1度だけ許可も含まれる) |
denied | 許可しない |
restricted | 端末の位置情報サービスが許可されていない |
notDetermined | 位置情報サービスを使用できるかどうかを選択していない |
因みに、requestWhenInUseAuthorization( )はnotDetermined以外だと呼ばれない
です。
これはリクエストを許可しているのにアラートが表示されてしまうのを防ぐためだと思われます。
ユーザーにリクエストしたのはいいですが、まだ現時点では位置情報を取得することは出来ません。
#4.デリゲートメソッドの実装
CLLocationManagerDelegateプロトコルを準拠し、デリゲートメソッドを実装していきます。
import CoreLocation
final class ViewController: UIViewController {
//CLLocationManagerクラスのインスタンスlocationManagerをメンバプロパティとして宣言
private var locationManager: CLLocationManager = {
// インスタンスを初期化
var locationManager = CLLocationManager()
// 取得精度の設定(1km以内の精度)
locationManager.desiredAccuracy = kCLLocationAccuracyKilometer
// 位置情報取得間隔(5m移動したら位置情報取得)
locationManager.distanceFilter = 5
return locationManager
}()
override func viewDidLoad() {
super.viewDidLoad()
// ユーザーに許可をリクエスト
locationManager.requestWhenInUseAuthorization()
// デリゲート先を自分のViewControllerにする
locationManager.delegate = self
}
}
// ViewControllerにCLLocationManagerDelegateプロトコルを準拠
extention ViewController: CLLocationManagerDelegate {
// CLLocationManagerクラスのインスタンス初期化および、認証ステータスが変更されたら呼ばれるメソッド
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
// アプリの現在の認証ステータス
let status = manager.authorizationStatus
switch status {
case .authorizedAlways, .authorizedWhenInUse:
// 位置情報取得を開始
manager.startUpdatingLocation()
case .notDetermined:
// ユーザーに許可をリクエスト
manager.requestWhenInUseAuthorization()
case .denied:
break
case .restricted:
break
default:
break
}
}
// 位置情報を取得・更新したら呼ばれるメソッド
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
// 位置情報を取得する
guard let gps = manager.location?.coordinate else {
// 取得出来なかったらアラートを表示したりなど
return
}
// 位置情報取得を停止
manager.stopUpdatingLocation()
//経度と緯度を出力する
let lat = gps.latitude
let lng = gps.longitude
print("経度:\(String(describing: lat)), 緯度:\(String(describing: lng))")
}
}
requestWhenInUseAuthorization()
によって、認証ステータスが決定したら
locationManagerDidChangeAuthorizationデリゲートメソッドが呼ばれ、そのメソッド内で処理していきます。
とりあえずは、ユーザーが.authorizedAlways
, .authorizedWhenInUse
を選択した場合の処理と
.notDetermined
を選択した場合の処理を記述しています。
位置情報を取得するためには、startUpdatingLocation()
メソッドを呼ばなければいけません。
このメソッドにより、位置情報取得を開始
してくれます。
そして、位置情報を取得したらlocationManager(_: didUpdateLocations:)デリゲートメソッドが呼ばれ、
ここでやっと位置情報を取得することが出来ます。
では、長くなりましたが、ここからが本題となります。
#本題
題名にも書いてあるように、位置情報を取得する時には配慮しなくてはいけない点がいくつかあります。
まずは、端末の位置情報サービスの有無
です。
端末の位置情報サービスがオフだった場合、「位置情報サービスをオンにして下さい」というアラートが
表示されると思いますが、アプリをインストール後の初回起動のみ
しか表示されません。
なので、2回目以降の起動時は端末の位置情報サービスがオフ
だった場合、何も起こりません。
もちろん、位置情報を取得することも出来ません。
では、どうすれば良いのでしょうか?
#端末の位置情報サービスの有無をチェックする
自分で端末の位置情報サービスの有無をチェックするコードを記述しなくてはいけません。
しかし、そんなに難しい訳ではなく、CLLocationManagerクラスには便利なメソッドがあります。
それは、locationServicesEnabled( )です。
このメソッドは、端末で位置情報サービスが有効になっているかどうかを示すBool値を返します。
trueの場合は位置情報サービスオン
、falseの場合は位置情報サービスオフ
では、このメソッドを先ほどのソースコードに組み込んでいきましょう。
そして、実際にアラートを表示したいので
私が過去にQiitaに投稿した記事のやり方でアラートを表示したいと思います↓
UIAlertControllerをファイルを分けて実装してみる
import CoreLocation
final class ViewController: UIViewController {
//CLLocationManagerクラスのインスタンスlocationManagerをメンバプロパティとして宣言
private var locationManager: CLLocationManager = {
// インスタンスを初期化
var locationManager = CLLocationManager()
// 取得精度の設定(1km以内の精度)
locationManager.desiredAccuracy = kCLLocationAccuracyKilometer
// 位置情報取得間隔(5m移動したら位置情報取得)
locationManager.distanceFilter = 5
return locationManager
}()
override func viewDidLoad() {
super.viewDidLoad()
// ユーザーに許可をリクエスト
locationManager.requestWhenInUseAuthorization()
// デリゲート先を自分のViewControllerクラスにする
locationManager.delegate = self
}
}
// ViewControllerにCLLocationManagerDelegateプロトコルを準拠
extention ViewController: CLLocationManagerDelegate {
// CLLocationManagerクラスのインスタンス初期化および、認証ステータスが変更されたら呼ばれるメソッド
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
// 端末の位置情報サービスがオンの場合
if CLLocationManager.locationServicesEnabled() {
// アプリの現在の認証ステータス
let status = manager.authorizationStatus
switch status {
case .authorizedAlways, .authorizedWhenInUse:
// 位置情報取得を開始
manager.startUpdatingLocation()
case .notDetermined:
// ユーザーに許可をリクエスト
manager.requestWhenInUseAuthorization()
case .denied:
break
case .restricted:
break
default:
break
}
// 端末の位置情報サービスがオフの場合
}else {
Alert.okAlert(vc: self, title: "位置情報サービスを\nオンにして下さい", message: "「設定」アプリ ⇒「プライバシー」⇒「位置情報サービス」からオンにできます")
}
}
// 位置情報を取得・更新したら呼ばれるメソッド
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
// 位置情報を取得する
guard let gps = manager.location?.coordinate else {
// 取得出来なかったらアラートを表示したりなど
return
}
// 位置情報取得を停止
manager.stopUpdatingLocation()
//経度と緯度を出力する
let lat = gps.latitude
let lng = gps.longitude
print("経度:\(String(describing: lat)), 緯度:\(String(describing: lng))")
}
}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager)
内で分岐させています。
位置情報サービスをオフ
の状態でビルドしてみると、ちゃんと表示されました。
とにかく、これでアプリは端末の位置情報サービスの有無
をチェックするようになりましたね。
これで一件落着!と思いますが、ここから更に想定して実装していかなければいけません。
例えば、ユーザーがアラートでOKを選択して位置情報サービスをオフ
にしたままアプリに戻ったとしましょう。
どうなると思いますか?
流れとしてはこんな感じ↓
位置情報サービスオフ > アプリ起動 > アラート表示(OKを選択) > 設定アプリを開く > 何もしないまま戻る > ???
このままだと、アプリは何もチェックしません
。
この解決方法はアプリがバックグラウンド状態
から戻った時に位置情報サービスの有無
をチェックすれば解決します。
では、どのようにするのかをまとめようと思いましたが、記事が長くなってしまったので後日、書こうと思います。
ここまで読んで下さって、ありがとうございます!
もし、ここは違うよ!というのがありましたら気軽にコメントして下さい。
ここまでのソースコードも載せておきます。
#ソースコード
import CoreLocation
final class ViewController: UIViewController {
private var locationManager: CLLocationManager = {
var locationManager = CLLocationManager()
locationManager.desiredAccuracy = kCLLocationAccuracyKilometer
locationManager.distanceFilter = 5
return locationManager
}()
override func viewDidLoad() {
super.viewDidLoad()
locationManager.requestWhenInUseAuthorization()
locationManager.delegate = self
}
}
extention ViewController: CLLocationManagerDelegate {
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
if CLLocationManager.locationServicesEnabled() {
let status = manager.authorizationStatus
switch status {
case .authorizedAlways, .authorizedWhenInUse:
manager.startUpdatingLocation()
case .notDetermined:
manager.requestWhenInUseAuthorization()
case .denied:
break
case .restricted:
break
default:
break
}
}else {
Alert.okAlert(vc: self, title: "位置情報サービスを\nオンにして下さい", message: "「設定」アプリ ⇒「プライバシー」⇒「位置情報サービス」からオンにできます")
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let gps = manager.location?.coordinate else {
return
}
manager.stopUpdatingLocation()
let lat = gps.latitude
let lng = gps.longitude
print("経度:\(String(describing: lat)), 緯度:\(String(describing: lng))")
}
}
import UIKit
final class Alert {
static func okAlert(vc: UIViewController,title: String, message: String, handler: ((UIAlertAction) -> Void)? = nil) {
let okAlertVC = UIAlertController(title: title, message: message, preferredStyle: .alert)
okAlertVC.addAction(UIAlertAction(title: "OK", style: .default, handler: handler))
vc.present(okAlertVC, animated: true, completion: nil)
}
}