【Swift 3】Apple Watchで位置情報取得して表示する

# 【Swift 3】Apple Watchで位置情報取得して表示する
## はじめに
巷ではいろいろ言われてますが，
Apple Watch の可能性を信じ，探求する派の者です。
皆様は Apple Watch 使ってますか？
私は初代と Series 2 を両手につけてます。
が，何ができるの？
って聞かれて答えに窮する場面も多いです。
自分でも色々作ってますが今はあくまでも個人用。

とりあえず Series 2 から GPS が内蔵されたので
本当に単独で位置情報取れるようになったのかどうか
サンプル(前作ってた Swift 2 系の使い回し)でも作るかー
ということで Swift 3 の勉強も兼ねて作りました。
実際に値が取れるのかは色々なとこに行って
確かめてきたのでまた別の記事に書きます。(↓書きました)

[Apple Watch Series 2 単独でGPSの値が取れるか確かめてみた](http://qiita.com/MilanistaDev/items/19cd223ab9940ab95832)

## 実装
今回は Swift 3 で行いました。
iPhone 側は内蔵 GPS を使って緯度経度を表示するだけのもの。
Apple Watch 側は，iPhone または，Series 2 から Apple Watch に
内蔵された GPS を用いて緯度経度を表示し，それだけだと寂しいから
マップに遷移して該当ポイントにピンを立てる実装になります。

サンプルアプリを GitHub に push しましたので
気になる方は clone してみてください。
(38mm の方は手元になくて動作確認してないです。すみません。)

[GitHub リンク](https://github.com/MilanistaDev/GPSCheckerforWatch)

```bash
$ cd 適切なディレクトリ
$ git clone git@github.com:MilanistaDev/GPSCheckerforWatch.git
```

### iOS 側の実装
iOS 側は詳細は新規性もないので省略します。

位置情報を許可するようにします。今回は使用中のみにします。
Info.plist に追加。

![iOS_00.png](https://qiita-image-store.s3.amazonaws.com/0/88266/15eacf5a-6ed5-f5b3-339e-5caa202aa03d.png "iOS_00.png")


```LocationManager``` は WatchKit 側でも使いたいので新規クラスを作りました。
一応サンプルコードは下記になります。特段特別なところはないです。
**When in use** と **Always** でアプリ次第で使い分けれるようにはしました。

```Swift:LocationManager.swift
import UIKit
import CoreLocation

let LMLocationUpdateNotification: String = "LMLocationUpdateNotification"
let LMLocationInfoKey: String = "LMLocationInfoKey"

class LocationManager: NSObject, CLLocationManagerDelegate {

    private var locationManager: CLLocationManager
    private var currentLocation: CLLocation!
    
    // Singleton
    struct Singleton {
        static let sharedInstance = LocationManager()
    }
    
    // MARK:- Initialized
    override init() {
        
        locationManager = CLLocationManager()
        locationManager.desiredAccuracy = kCLLocationAccuracyBest
        locationManager.distanceFilter = 1000
        super.init()
        
        locationManager.delegate = self

        // Check Authorization(iOS8 and Later)
        // 位置情報認証状態をチェックしてまだ決まってなければアラート出す
        let status = CLLocationManager.authorizationStatus()
        if(status == CLAuthorizationStatus.notDetermined) {
            // Always
//            if (self.locationManager.responds(to: #selector(CLLocationManager.requestAlwaysAuthorization))) {
//                self.locationManager.requestAlwaysAuthorization()
//            }
            // When in Use
            if (self.locationManager.responds(to: #selector(CLLocationManager.requestWhenInUseAuthorization))) {
                self.locationManager.requestWhenInUseAuthorization()
            }
        }
        
        // if App uses background mode(iOS9 and later)
        // 位置情報をバックグラウンドで取得する際に必要
        // バックグラウンドのトグル入れる
        if #available(iOS 9.0, *) {
            //locationManager.allowsBackgroundLocationUpdates = true
        }
    }
    
    // MARK: - CLLocationManagerDelegate Method
    /**
       位置情報取得を開始
       Start updating location data
     */
    func startUpdatingLocation()
    {
        self.locationManager.startUpdatingLocation()
    }
    
    /**
       位置情報取得失敗時に呼ばれる
       Call when iOS device failed to get location data.
     */
    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        // print("Error: \(error.localizedDescription)")
        // Implement Alert
    }

    /**
       位置情報取得成功したときに呼ばれる
       Call when iOS device succeeded to get location data.
     */
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations:[CLLocation]) {
        
        let locationData = locations.last as CLLocation!
        self.currentLocation = locationData
        let locationDataDic = [LMLocationInfoKey : self.currentLocation]
        
        // Notice and send location data. | 通知して位置情報を送信
        let center = NotificationCenter.default
        center.post(name: NSNotification.Name(rawValue: LMLocationUpdateNotification), object: self, userInfo: locationDataDic)
        
        self.locationManager.stopUpdatingLocation()
    }
    
    /**
       位置情報の認証ステータスの変更時に呼ばれる
       Call when Authorization status changed.
     */
    private func locationManager(manager: CLLocationManager, didChangeAuthorizationStatus status:CLAuthorizationStatus) {
        if (status == .notDetermined) {
            // Always
//            if (self.locationManager.responds(to:#selector(CLLocationManager.requestAlwaysAuthorization))) {
//                self.locationManager.requestAlwaysAuthorization()
//            }
            // When in Use
            if (self.locationManager.responds(to: #selector(CLLocationManager.requestWhenInUseAuthorization))) {
                self.locationManager.requestWhenInUseAuthorization()
            }
        }
    }
}
```


### Watch 側の実装
#### 1. Watch 用の Target を追加

**File -> New->Target…**
watchOS の WatchKit App を選択して Nextを押す。

![watchOS_00.png](https://qiita-image-store.s3.amazonaws.com/0/88266/f9fe0409-b0bd-5946-33ee-8452b1f00c10.png "watchOS_00.png")

Notification Scene，Complication は今回触りません。
チェック入れると ```Interface.storyboard``` に画面が追加されます。

![watchOS_01.png](https://qiita-image-store.s3.amazonaws.com/0/88266/3dd88a97-83c1-ccbd-90aa-af374701bd22.png "watchOS_01.png")


一応 Activate しておく。

![watchOS_02.png](https://qiita-image-store.s3.amazonaws.com/0/88266/e15870d0-2e16-1d5f-6a95-ab7d64aa3ec1.png "watchOS_02.png")


Target を Extension の方にして info にこちらにも
**Privacy - Location When In Use Usage Description** を追加。

![watchOS_03.png](https://qiita-image-store.s3.amazonaws.com/0/88266/9c65bb3a-49a5-4182-5bdc-713369aa3920.png "watchOS_03.png")


ここで起動して見ると iPhone 側にアラートが出る。

![watchOS_04.png](https://qiita-image-store.s3.amazonaws.com/0/88266/d10c22a4-d324-656c-9ad0-c5a7aad5bcec.png "watchOS_04.png")



#### 2. Interface.storyboard の実装
AutoLayout とか考えずに(実際は似たものが必要)パズルみたいに配置。
新しい Interface Controller の画面を追加して push で紐付ける。
(地図表示用にする)このとき **Segue Identifier** をつける。

![watchOS_05.png](https://qiita-image-store.s3.amazonaws.com/0/88266/54c8a2ae-b743-dc4d-eeff-d005e8a7732f.png "watchOS_05.png")


プロパティを ```InterfaceController.swift``` に紐付ける。
ファイルは Extension の方にある。
ここでは緯度経度表示用のラベル 2 つを紐付けました。
プロパティ名は ```latLabel``` と ```lonLabel``` にしました。

![watchOS_06.png](https://qiita-image-store.s3.amazonaws.com/0/88266/64b97111-df71-00fb-0105-59de65cae1a5.png "watchOS_06.png")

**File -> New -> File…** で
Map 用の InterfaceController を追加します。
watchOS の欄から選ぶようにする。

![watchOS_07.png](https://qiita-image-store.s3.amazonaws.com/0/88266/b9c0e9c8-c08b-d36d-51d2-b7a3e6ea0561.png "watchOS_07.png")

名前は ```MapInterfaceController``` にしました。

![watchOS_08.png](https://qiita-image-store.s3.amazonaws.com/0/88266/5a793a9c-99ee-1b82-f35b-1031b3309362.png "watchOS_08.png")


Map を追加した画面に MapView を配置し，
クラスを追加した ```MapInterfaceController``` にする。

![watchOS_09.png](https://qiita-image-store.s3.amazonaws.com/0/88266/11ab5320-14e8-bda6-aff5-e11c42fb8f8d.png "watchOS_09.png")


MapView を ```MapInterfaceController``` に紐付ける。
プロパティ名は ```mapView``` にしました。

![watchOS_10.png](https://qiita-image-store.s3.amazonaws.com/0/88266/3a4a9611-aad6-d622-cdda-15ab4bd19140.png "watchOS_10.png")



#### 3. 位置情報を取得して表示する
```InterfaceController``` と ```MapInterfaceController``` に実装していく。
前者は位置情報を取得して表示させること，ボタンが押下されたときに
緯度と経度の数値を ```MapInterfaceController``` に渡す実装をします。

後者は，位置情報を受け取って，地図にピン刺しする実装になります。

前者の実装は下記になります。
iOS 側と同じ ```LocationManager.swift``` を使い，
位置情報が更新されたら通知がくるので，通知を受けて表示メソッドが，
位置情報を抜き出してラベルに表示する。

```Swift:InterfaceController.swift
import WatchKit
import Foundation
import CoreLocation

class InterfaceController: WKInterfaceController {

    // MARK:- Property

    @IBOutlet var latLabel: WKInterfaceLabel!
    @IBOutlet var lonLabel: WKInterfaceLabel!
    var latValue:Double = 100.0 // Impossible value(-90 to 90)
    var lonValue:Double = 200.0 // Impossible value(-180 to 180)

    // MARK:- Life Cycle

    override func awake(withContext context: Any?) {
        super.awake(withContext: context)
        // Configure interface objects here.
    }
    
    override func willActivate() {
        super.willActivate()

        // Access Location Service
        LocationManager.Singleton.sharedInstance.startUpdatingLocation()

        // Set NSNotification
        let center = NotificationCenter.default
        center.addObserver(self,
                           selector:#selector(displayData(notification:)),
                           name:NSNotification.Name(rawValue: LMLocationUpdateNotification),
                           object:nil)
    }
    
    override func didDeactivate() {
        // This method is called when watch view controller is no longer visible
        super.didDeactivate()
    }

    deinit {
    	 // 通知の解除
        let center = NotificationCenter.default
        center.removeObserver(self)
    }

    // MARK:- Private Method

    /**
     取得した位置情報から緯度経度をAppleWatchのLabelに表示
     Acquired latitude and longitude values are displayed on the Apple Watch's labels.
     - parameter notification:通知
     */
    func displayData(notification:Notification) {
        let infoDic: Dictionary = notification.userInfo as Dictionary!
        let location: CLLocation? = infoDic[LMLocationInfoKey] as? CLLocation
        let coordinate = location!.coordinate

        self.latValue = coordinate.latitude
        self.lonValue = coordinate.longitude

        self.latLabel.setText((coordinate.latitude).description)
        self.lonLabel.setText((coordinate.longitude).description)
    }
}
```

ここで LocationManager の警告が出ます。

![watchOS_11.png](https://qiita-image-store.s3.amazonaws.com/0/88266/1ab9608c-dbfd-4a64-28d8-260c1037a7a2.png "watchOS_11.png")


Extension 側でファイルが使用できる状態でないためで，
```LocationManager.swift``` の **Target MemberShip** に チェックを追加する。

![watchOS_12.png](https://qiita-image-store.s3.amazonaws.com/0/88266/11de6d53-4e6f-68a6-d16b-a92cec5c37a9.png "watchOS_12.png")


#### 4. ボタン押下して，緯度経度の値を値渡し

あとは，ボタンを押したときに値を渡せるように実装します。
やり方は複数ありますが，ここでは Segue Identifier を設定しましたので
**contextForSegue** を使うようにします。
ここら辺が，iOS 側とメソッド名が違うので少し戸惑うところかもしれません。
緯度経度の値チェックの仕方が良い方法が思いつかなかったのでありえない数値で
初期化してそれでチェックかけてます。もっと良い方法ありそうです。

また **Segue Identifier** が正しいかをチェックして緯度経度の値を渡している。

```Swift:InterfaceController.swift
/**
 緯度と経度を遷移先のマップ画面に渡す
 Pass the latitude and longitude values to the next map screen(VC).
 - parameter segueIdentifier: SegueのIdentifier名
 - returns Any
 */
override func contextForSegue(withIdentifier segueIdentifier: String) -> Any? {

    guard self.latValue != 100.0 && self.lonValue != 200.0 else {
        return nil // 値が取れなかったとき
    }
    guard segueIdentifier == "displayMapSegue" else {
        return nil // 入らない認識
    }
    let locationData: [String : Double] = ["latitude": self.latValue, "longitude" : self.lonValue]
    
    return locationData
}
```

#### 5. 受け取った緯度経度を使って Map にピン刺し

緯度経度の値を受け取って Map にピンを立てるようにした。

```MapInterfaceController``` 側では値を受けるプロパティを用意して，
**context** が ```nil``` でないことを確認して nil だったら
前の画面に戻す処理を書いた。MapKit に関することは割愛します。

```Swift:MapInterfaceController.swift
import WatchKit
import Foundation

class MapInterfaceController: WKInterfaceController {

    // MARK:- Property

    @IBOutlet var mapView: WKInterfaceMap!
    var locationData: [String : Double] = [:]

    // MARK:- Life Cycle

    override func awake(withContext context: Any?) {
        super.awake(withContext: context)
        
        guard let contextData = context else {
            // 受け取った context が nil だったら前の画面に戻す
            popToRootController()
            return
        }
        self.locationData = contextData as! [String : Double]
        let latValue = locationData["latitude"]
        let lonValue = locationData["longitude"]

        let mapLocation = CLLocationCoordinate2DMake(latValue!, lonValue!)
        let coordinateSpan = MKCoordinateSpanMake(0.02, 0.02)

        self.mapView.addAnnotation(mapLocation, with: WKInterfaceMapPinColor.red)
        self.mapView.setRegion(MKCoordinateRegionMake(mapLocation, coordinateSpan))
    }

    override func willActivate() {
        // This method is called when watch view controller is about to be visible to user
        super.willActivate()
    }

    override func didDeactivate() {
        // This method is called when watch view controller is no longer visible
        super.didDeactivate()
    }
}
```

## 実行結果
実機の方がいいですが，シミュレータでも位置情報を付加すれば確認できます。

この記事書いてるのは南砂町のスタバですが，
例えばここで実行するとこんな感じになります。
![GPSCheckerforWatch.PNG](https://qiita-image-store.s3.amazonaws.com/0/88266/74846d18-4d0a-9000-713c-59cb2503feff.png "GPSCheckerforWatch.PNG")

![GPSCheckerforWatchMap.PNG](https://qiita-image-store.s3.amazonaws.com/0/88266/a89cefd6-4fb2-5f9d-3273-5d89c22eb6e3.png "GPSCheckerforWatchMap.PNG")

## おわりに

今回は，Apple Watch で位置情報を表示させて，
ついでに Map にピン刺しする実装について書きました。
次回は実際にどういう状態で値が取れるのかについて，
そして試しにいろいろなところに行って確認してみたので
そのことについて書きます。

ここまでご覧いただきありがとうございました！
緯度経度の初期化値は何が適切なのか，またこうした方がいい，
ここは間違っているなど，ご指摘お願いいたします。

※ <u>ブログ用に執筆したものを md 化したものです。</u>