WidgetKitで天気予報アプリ作ってみた〜位置情報取得&保存編〜

## 投稿の経緯
前回投稿した**WidgetKitで天気予報アプリ作ってみた~天気情報取得編~**の続編です。
今回は位置情報を取得して保存するところまでを記事にしようと思います。

前回の記事を見てない方は先に↓こちら↓を確認してください。

https://qiita.com/nkekisasa222/items/359192455b65fb60705d

## 開発環境
Swift 5.5
Xcode 13.2.1

## サンプルプロジェクト
GitHubにPushしています。気になる方はご覧ください。
https://github.com/ken-sasaki-222/WeatherWidget
![QR_615642.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643372/8c43013f-9757-f7dd-5946-2684f30f63b7.png)

## 位置情報取得
今回はウィジェットの開発がメインなので、位置情報の取得はあまり作り込まずに進めたいと思います。
#### CoreLocationを追加
![f1d514039f04c23c7a3d6fc74ef01941.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643372/b5cee192-b8a9-f261-c980-b6df35906ce7.png)
Targets > Build Phases > Link Binaries with LibrariesにCoreLocationを追加。

#### Info.plistへ追加
![ff1ce19d65839fd746c585eac081cc16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643372/8eb59316-2cdc-de25-8569-6382abe851d3.png)
`Praivacy - Location When In Use Usage Description`を追加してアラートに表示するテキストを設定します。

#### AppDelegateを追加
```WeatherWidgetApp.swift
import SwiftUI

@main
struct WeatherWidgetApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}
```

```AppDelegate.swift
import SwiftUI

class AppDelegate: UIResponder, UIApplicationDelegate {
    private let locationManagerHelper = LocationManagerHelper()
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        locationManagerHelper.callLocationManager()
        
        return true
    }
}
```
`@UIApplicationDelegateAdaptor`を使って`AppDelegate`を用意します。今回はアプリ起動時に位置情報を選択してない場合、位置情報許諾アラートを表示するぐらいの実装でいいので、`didFinishLaunchingWithOptions`の中で位置情報を扱うHelperクラスを呼びます。

#### 位置情報を扱うHelperクラスを追加
```LocationManagerHelper.swift
import CoreLocation

class LocationManagerHelper: NSObject, CLLocationManagerDelegate {
    private var locationManager: CLLocationManager
    
    override init() {
        self.locationManager = CLLocationManager()
        super.init()
        self.locationManager.delegate = self
    }
    
    func callLocationManager() {
        locationManager.requestWhenInUseAuthorization()
        
        if CLLocationManager.locationServicesEnabled() {
            locationManager.pausesLocationUpdatesAutomatically = false
            locationManager.desiredAccuracy = kCLLocationAccuracyKilometer
            locationManager.startUpdatingLocation()
        }
    }
    
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        guard let newLocation = locations.last else {
            return
        }
        
        let location = CLLocationCoordinate2D(
            latitude: newLocation.coordinate.latitude,
            longitude: newLocation.coordinate.longitude
        )
        
        print("緯度:", location.latitude, "経度:", location.longitude)       
        locationManager.stopUpdatingLocation()
    }
}
```
位置情報を取得する処理です。先に記載したように、今回はLocationManagerに関する処理をあまり作り込んでいませんので、Helperクラスを用意してその中に位置情報を扱う処理を逃しています。

続いて、取得した位置情報を`UserDefaults`に保存していきます。

## 位置情報保存
[WidgetKitで天気予報アプリ作ってみた〜天気情報取得編〜](https://qiita.com/nkekisasa222/items/359192455b65fb60705d)で書いているように今回はアーキテクチャにリポジトリパターンを採用しています。UserDefaultsとのやりとりはRepositoryを経由してDataStoreに任せようと思います。
#### DataStore
```UserDefaultsDataStore.swift
import Foundation

final class UserDefaultsDataStore {
    
    private enum DefaultsKey: String {
        case lat
        case lng
    }
    
    private var defaults: UserDefaults {
        UserDefaults.standard
    }
    
    var lat: Double {
        get {
            defaults.double(forKey: DefaultsKey.lat.rawValue)
        }
        set(newValue) {
            defaults.set(newValue, forKey: DefaultsKey.lat.rawValue)
        }
    }
    
    var lng: Double {
        get {
            defaults.double(forKey: DefaultsKey.lng.rawValue)
        }
        set(newValue) {
            defaults.set(newValue, forKey: DefaultsKey.lng.rawValue)
        }
    }
}
```
UserDefaultsとやりとりをするDataStoreです。緯度経度の保存と取り出しを担当し、取り出した値をRepositoryへ返します。

#### Repository
```UserRepositoryInterface.swift
import Foundation

protocol UserRepositoryInterface {
    var lat: Double { get set }
    var lng: Double { get set }
}
```
```UserRepository.swift
import Foundation

class UserRepository: UserRepositoryInterface {
    private let userDefaultsDataStore = UserDefaultsDataStore()
    
    var lat: Double {
        get {
            userDefaultsDataStore.lat
        }
        set(newValue) {
            userDefaultsDataStore.lat = newValue
        }
    }
    
    var lng: Double {
        get {
            userDefaultsDataStore.lng
        }
        set(newValue) {
            userDefaultsDataStore.lng = newValue
        }
    }
}
```
UserDefaultsDataStoreとやりとりをするRepositoryです。緯度経度の取得と保存は必ずこのRepositoryを経由します。

#### Helperクラスを書き換える
```RepositoryRocator.swift
import Foundation

class RepositoryRocator {
    
    static func getWeatherRepository() -> WeatherRepositoryInterface {
        WeatherRepository()
    }
    
    static func getUserRepository() -> UserRepositoryInterface {
        UserRepository()
    }
}
```
```LocationManagerHelper.swift
import CoreLocation

class LocationManagerHelper: NSObject, CLLocationManagerDelegate {
    private var userRepository: UserRepositoryInterface
    private var locationManager: CLLocationManager
    
    init(userRepository: UserRepositoryInterface) {
        self.userRepository = userRepository
        self.locationManager = CLLocationManager()
        super.init()
        self.locationManager.delegate = self
    }
    
    override convenience init() {
        self.init(userRepository: RepositoryRocator.getUserRepository())
    }
    
    func callLocationManager() {
        locationManager.requestWhenInUseAuthorization()
        
        if CLLocationManager.locationServicesEnabled() {
            locationManager.pausesLocationUpdatesAutomatically = false
            locationManager.desiredAccuracy = kCLLocationAccuracyKilometer
            locationManager.startUpdatingLocation()
        }
    }
    
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        guard let newLocation = locations.last else {
            return
        }
        
        let location = CLLocationCoordinate2D(
            latitude: newLocation.coordinate.latitude,
            longitude: newLocation.coordinate.longitude
        )
        
        print("緯度:", location.latitude, "経度:", location.longitude)
        
        userRepository.lat = location.latitude
        userRepository.lng = location.longitude
        
        locationManager.stopUpdatingLocation()
    }
}
```
RepositoryLocatorを経由してUserRepositoryを取得しています。位置情報が更新されたタイミングで緯度経度を保存するように書き換えました。

## 保存した位置情報でリクエストを送る
```WeatherViewModel.swift
import Foundation

class WeatherViewModel: NSObject {
    private let weatherRepository: WeatherRepositoryInterface
    private let userRepository: UserRepositoryInterface
    
    init(weatherRepository: WeatherRepositoryInterface, userRepository: UserRepositoryInterface) {
        self.weatherRepository = weatherRepository
        self.userRepository = userRepository
        super.init()
    }
    
    override convenience init() {
        self.init(weatherRepository: RepositoryRocator.getWeatherRepository(), userRepository: RepositoryRocator.getUserRepository())
    }
    
    func createRequestModel() -> WeatherRequestModel {
        let requestModel = WeatherRequestModel(
            lat: userRepository.lat,
            lng: userRepository.lng
        )
        
        return requestModel
    }
    
    func fetchWeathers() async {
        do {
            let response = try await weatherRepository.fetchWeathers(requestModel: createRequestModel())
            print("Success fetch weathers:", response.hourly)
        }
        catch {
            print("Error fetch weathers:", error)
        }
    }
}
```
[WidgetKitで天気予報アプリ作ってみた〜天気情報取得編〜](https://qiita.com/nkekisasa222/items/359192455b65fb60705d)の時点では緯度経度を直接指定していましたが、保存した緯度経度を使って`WeatherRequestModel`を作るように書き換えました。これで現在地から取得した位置情報を使ってリクエストを送れるようになりました。

## おわりに
今回は**WidgetKitで天気予報アプリ作ってみた~位置情報取得&保存編~**について書きました。
次はいよいよウィジェットの開発です！

続きが気になる方は↓こちら↓から

https://qiita.com/nkekisasa222/items/744754523172035cc9bf

ご覧いただきありがとうございました。
こうしたほうがいいや、ここはちょっと違うなど気になる箇所があった場合、ご教示いただけると幸いです。

## お知らせ
現在副業でiOSアプリ開発案件を募集しています。
[Twitter DM](https://twitter.com/ken_sasaki2)でご依頼お待ちしております！
![QR_615427.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643372/4f8dff15-33fa-c042-44a3-b592f49c959c.png)
↓活動リンクはこちら↓
https://linktr.ee/sasaki.ken