Edited at

Swiftで地図アプリを実装する

More than 3 years have passed since last update.


学習内容

・MapKitの基本的な使い方を学び、アプリに地図を表示する

・CoreLoationの基本的な使い方を学び、現在地を取得する


概要

・MapKitライブラリを使うことにより、アプリ内で簡単に地図を表示することができるようになります。

ここでは、MapKitを使い簡単な地図アプリを実装してみましょう。


実装手順

1.ファイルにMapKitをインポートし、MapViewを表示する

2.Viewに長押しを探知するジェスチャーを追加し、長押しした箇所にピンを立てるようにする

3.ファイルにCoreLocationをインポートし、アプリ起動時に現在地を取得し、ピンを立てる

4.アプリ起動時に、現在地周辺の詳細を表示できるように、マップを拡大する

このような手順でアプリを実装していきたいと思います。


1.MapViewを表示する

import UIKit

//MapKitのインポート
import MapKit

class ViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()
//MapViewを生成し、表示する
let myMapView = MKMapView()
myMapView.frame = self.view.frame
self.view.addSubview(myMapView)
}
}

ここで一度、アプリを起動してみると地図が表示されると思います。


2.長押しした場所にピンを立てる

viewDidLoadの中に次の記述を追加してください。

//長押しを探知する機能を追加

//ジェスチャーの生成
let longPressGesture = UILongPressGestureRecognizer()
//ボタンを押したときの処理
longPressGesture.addTarget(self, action: "longPressed:")
myMapView.addGestureRecognizer(longPressGesture)

この3行の記述でmyMapViewにジェスチャーを追加しています。

※「:(コロン)」のつけ忘れに注意してください。

myMapViewにジェスチャーを追加しましたら、次にジェスチャーのactionを作っていきます。

viewDidLoadの外に以下のように記述してください。

//長押しした時にピンを置く処理

func longPressed(sender: UILongPressGestureRecognizer) {

//この処理を書くことにより、指を離したときだけ反応するようにする(何回も呼び出されないようになる。最後の話したタイミングで呼ばれる)
if sender.state != UIGestureRecognizerState.Began {
return
}

//senderから長押しした地図上の座標を取得
let tappedLocation = sender.locationInView(myMapView)
let tappedPoint = myMapView.convertPoint(tappedLocation, toCoordinateFromView: myMapView)

//ピンの生成
let pin = MKPointAnnotation()
//ピンを置く場所を指定
pin.coordinate = tappedPoint
//ピンのタイトルを設定
pin.title = "タイトル"
//ピンのサブタイトルの設定
pin.subtitle = "サブタイトル"
//ピンをMapViewの上に置く
self.myMapView.addAnnotation(pin)

}

おそらくmyMapViewの箇所でエラーが出たと思います。

「Use of unresolved identifier 'myMapView'」というエラー文です。

myMapViewが定義されていませんよ、という意味のエラーになります。

しかし、先ほど定義したはずですね。

実はmyMapViewをviewDidLoadの中で定義してたので、このままだと他の関数の中で使えません。

他の関数の中でもmyMapViewにアクセスできるように、先ほど定義したmyMapViewをクラスのプロパティにしてあげましょう。

以下の通りです。

class ViewController: UIViewController {

let myMapView = MKMapView() //クラスの外で宣言

エラーが解決できたので、関数内の処理について一つ一つ順に説明していきます。

さっそく見慣れない処理が出てきましたね。

if sender.state != UIGestureRecognizerState.Began {

return
}

まずこの処理が何のためにあるのか、確認してみたいと思います。

この3行をコメントアウト(command+/)し、かわりに

print("Hello World")

このように記述し、アプリを実行してみてください。

そして、画面を長押ししてみましょう。

画面を長押ししてある間、デバックエリアに

「Hello World」

という文字が、何回も現れたと思います。

実は

if sender.state != UIGestureRecognizerState.Began {

return
}

この記述をすることでUIGestureRecognizerState.Began以外のときはreturnし、以下の処理を行わないようにしています。

次にここの処理です。

//ピンの生成

let pin = MKPointAnnotation()
//ピンを置く場所を指定
pin.coordinate = tappedPoint
//ピンのタイトルを設定
pin.title = "タイトル"
//ピンのサブタイトルの設定
pin.subtitle = "サブタイトル"
//ピンをMapViewの上に置く
self.myMapView.addAnnotation(pin)

ここではピンの生成をし、myMapViewに配置していますね。

流れは以下の通りです。

1.ピンを生成

2.ピンの座標を指定する

3.ピンにタイトル、サブタイトルをつける

4.MapViewの上に置く

coordinateという見慣れない言葉が出てきました。

coordinateには位置情報を代入してあげます。

位置情報とは、Latitude(経度)とLongitude(緯度)です。

今回は

pin.coordinate = tappedPoint

という形でtappedPointを代入しています。

それでは、tappedPointを宣言している箇所を見てみましょう。

let  tappedLocation = sender.locationInView(myMapView)

let tappedPoint = myMapView.convertPoint(tappedLocation, toCoordinateFromView: myMapView)

senderを使い、myMapView上の位置を取得しています。

しかし、まだこの情報はアプリのView上の位置であるので、

convertPointメソッドを使い、Coordinateに代入できる形(CLLocationCoordinate2D)に変換してあげます。

ここまで確認したところで、アプリを実行して動作を確認してください。

画面を長押ししたタイミングでマップ上にピンが生成されていれば問題ありません。


3-1.アプリ起動時に現在地にピンを立てる

次にアプリを起動したタイミングで、現在地にピンを立てたいと思います。

そのためには、現在地を取得する必要がありますね。

そのときに使うのが

CoreLocationです。

ファイルにCoreLocationをインポートしましょう。

import CoreLocation

CoreLocationを使うことで現在地を取得することができます。

しかし、これだけではCoreLocationを使用することができません。

info.plistを次のように編集してあげる必要があります。


Key: NSLocationAlwaysUsageDescription

Type: String

Value: Use CoreLocation!(ここの文字は何でも構いません)


info.plistの編集が終わりましたら次のようにコードを編集してください。

class ViewController: UIViewController, CLLocationManagerDelegate {

let myLocationManager = CLLocationManager()

override func viewDidLoad() {
super.viewDidLoad()
self.myLocationManager.delegate = self

//セキュリティ認証のステータスを取得
let status = CLLocationManager.authorizationStatus()
if status == CLAuthorizationStatus.NotDetermined {
// まだ承認が得られていない場合は、認証ダイアログを表示
myLocationManager.requestAlwaysAuthorization()
}
//現在地取得の開始
myLocationManager.startUpdatingLocation()
}
//現在地の取得に成功した場合の処理
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
print("現在地の取得に成功しました")

}
//現在地の取得に失敗した場合の処理
func locationManager(manager: CLLocationManager, didFailWithError error: NSError) {
print("現在地の取得に失敗しました")
}

CoreLocationの使い方は形式的なものになるので、簡単に使い方のみ説明します。

①クラスのプロパティとしてCLLocationManagerを宣言

②デリゲートを宣言し、デリゲート先をselfに指定する

③セキュリティ状態の確認

④現在地に取得を開始

⑤現在地の取得に成功した場合、失敗した場合に実行されるデリゲートメソッドをそれぞれ定義する

これがCoreLocationを使用する際、最低限しなくてはならない処理です。

③について、少し補足します。

アプリを使用する際、携帯の持ち主の許可なくGPSにアクセスすることはできません。

//セキュリティ認証のステータスを取得

let status = CLLocationManager.authorizationStatus()
if status == CLAuthorizationStatus.NotDetermined {
// まだ承認が得られていない場合は、認証ダイアログを表示
myLocationManager.requestAlwaysAuthorization()
}

そのためこのような処理が必要になります。

まず、現在のGPS利用許可の状況を取得し、

if文で場合分けします。

もし許可が得られていない(NotDetermined)だった場合、

常にGPS利用を許可するようにリクエストします。

ここまで出来たらアプリを実行してみましょう。

※ただしシュミレーターで起動する際には、デバックエリア付近にある「Simulate Location」で地域を選択してあげなくてはなりません。


3-2現在地を取得した後の処理

// GPSから値を取得した際に呼び出されるメソッド

func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
// 配列から現在座標を取得(配列locationsの中から最新のものを取得する)
let myLocation = locations.last! as CLLocation
//Pinに表示するためにはCLLocationCoordinate2Dに変換してあげる必要がある
let currentLocation = myLocation.coordinate
//ピンの生成と配置
let pin = MKPointAnnotation()
pin.coordinate = currentLocation
pin.title = "現在地"
self.myMapView.addAnnotation(pin)
}

ここは先ほどのピンの生成の処理をほとんど変わりません。

ただ、ピンの位置を取得の仕方が少し変わりました。

取得した配列の中の最新のものを.lastで取得しています。

それをCLLocationにダウンキャストし、myLocationに代入しています。

let myLocation = locations.last! as CLLocation

さらに、myLocationをcoordinateに代入できるように、変換してあげています。その値をcurrentLocationとし、

let currentLocation = myLocation.coordinate

中略
pin.coordinate = currentLocation

でピンに位置情報を渡してあげています。

ここまで記述できましたら、アプリを起動してみましょう。

起動したタイミングで現在地を取得し、ピンが立てられたと思います。

(※シュミレーターで起動する際は、Simulate Locationで現在地を指定してあげる必要があります。)


4.アプリ起動時にマップを拡大する

ここまででアプリの基本的な機能は完成しました。

しかし、今のままだと、アプリ起動時に日本全体が表示されていて、地図アプリっぽくないですね。

最後に、起動時にマップを拡大してあげる処理を行いましょう。

位置情報の取得に成功したときに呼ばれるデリゲートメソッド内に記述していきます。

// GPSから値を取得した際に呼び出されるメソッド

func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
  (前略
//アプリ起動時の表示領域の設定
//delta数字を大きくすると表示領域も広がる。数字を小さくするとより詳細な地図が得られる。
let mySpan = MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
let myRegion = MKCoordinateRegionMake(currentLocation, mySpan)
myMapView.region = myRegion
}

拡大された地図を表示するためには、

MapViewにRegion(地域)を指定してあげる必要があります。

myMapView.region = myRegion

Regionを作るためには、SpanとCoordinateが必要です。

それではまず、Spanを作りましょう。

Spanというのは、地図をどのくらい拡大して表示するかです。

引数の値を大きくすればするほど、広域の地図になります。

逆に小さくすれば、より詳細な地図が表示されるようになります。

今回は0.05くらいにしてあげましょう。

let mySpan = MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)

Coordinate(位置情報)は先ほど取得した現在地の情報を利用します。

この2つの情報をもとに、Regionを作りましょう。

let myRegion = MKCoordinateRegionMake(currentLocation, mySpan)

以上です。これでアプリ起動時に拡大されたマップが表示されると思います。

最後に完成したコードを載せておきます。


完成したコード

import UIKit

import MapKit
import CoreLocation

//シュミレーター上でMapの拡大縮小は、Optionを押しながらトラックパッドを操作することで可能

//Info.plistの編集
// Key: NSLocationAlwaysUsageDescription
// Type: String
// Value: Use CoreLocation!

class ViewController: UIViewController, CLLocationManagerDelegate {

//MapViewの生成
let myMapView = MKMapView()

//LocationManagerの生成(viewDidLoadの外に指定してあげることで、デリゲートメソッドの中でもmyLocationManagerを使用できる)
let myLocationManager = CLLocationManager()

override func viewDidLoad() {
super.viewDidLoad()

//ここからがMapView生成の処理
//.frameでサイズと位置を指定する
myMapView.frame = self.view.frame
self.view.addSubview(myMapView)

//長押しを探知する機能を追加
let longTapGesture = UILongPressGestureRecognizer()
longTapGesture.addTarget(self, action: "longPressed:")
myMapView.addGestureRecognizer(longTapGesture)

//ここからが現在地取得の処理
myLocationManager.delegate = self

// セキュリティ認証のステータスを取得
let status = CLLocationManager.authorizationStatus()
if status == CLAuthorizationStatus.NotDetermined {
// まだ承認が得られていない場合は、認証ダイアログを表示
myLocationManager.requestAlwaysAuthorization()
}
// 位置情報の更新を開始
myLocationManager.startUpdatingLocation()
}

override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}

//長押しした時にピンを置く処理
func longPressed(sender: UILongPressGestureRecognizer) {

//この処理を書くことにより、指を離したときだけ反応するようにする(何回も呼び出されないようになる。最後の話したタイミングで呼ばれる)
if sender.state != UIGestureRecognizerState.Began {
return
}

//senderから長押しした地図上の座標を取得
let tappedLocation = sender.locationInView(myMapView)
let tappedPoint = myMapView.convertPoint(tappedLocation, toCoordinateFromView: myMapView)

//ピンの生成
let pin = MKPointAnnotation()
//ピンを置く場所を指定
pin.coordinate = tappedPoint
//ピンのタイトルを設定
pin.title = "タイトル"
//ピンのサブタイトルの設定
pin.subtitle = "サブタイトル"
//ピンをMapViewの上に置く
self.myMapView.addAnnotation(pin)
}

// GPSから値を取得した際に呼び出されるメソッド
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
// 配列から現在座標を取得(配列locationsの中から最新のものを取得する)
let myLocation = locations.last! as CLLocation
//Pinに表示するためにはCLLocationCoordinate2Dに変換してあげる必要がある
let currentLocation = myLocation.coordinate
//ピンの生成と配置
let pin = MKPointAnnotation()
pin.coordinate = currentLocation
pin.title = "現在地"
self.myMapView.addAnnotation(pin)

//アプリ起動時の表示領域の設定
//delta数字を大きくすると表示領域も広がる。数字を小さくするとより詳細な地図が得られる。
let mySpan = MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
let myRegion = MKCoordinateRegionMake(currentLocation, mySpan)
myMapView.region = myRegion
}

//GPSの取得に失敗したときの処理
func locationManager(manager: CLLocationManager, didFailWithError error: NSError) {
print(error)
}

//認証状態を確認するだけなので、ここの処理はなくてもOK
func locationManager(manager: CLLocationManager, didChangeAuthorizationStatus status: CLAuthorizationStatus) {
// 認証のステータスをログで表示.
var statusStr = ""
switch (status) {
case .NotDetermined:
statusStr = "NotDetermined"
case .Restricted:
statusStr = "Restricted"
case .Denied:
statusStr = "Denied"
case .AuthorizedAlways:
statusStr = "AuthorizedAlways"
case .AuthorizedWhenInUse:
statusStr = "AuthorizedWhenInUse"
}
print(" CLAuthorizationStatus: \(statusStr)")

}
}

お疲れ様でした!!