2021/04/24

SwiftUIでGoogle Maps SDKを使う

swiftgoogle-map

概要

Google MapをSwiftUIでお試し実装しました。

公式チュートリアルはSwiftUIではないのですが、それを読み替えてSwiftUIでチュートリアルを実施してみました。

実装内容

API keyの取得

  • ここを参考に
    • GCPプロジェクトを作成
    • APIを作成にして、アクセス制限等をつける
    • APIキーを取得する

必要なライブラリのインストール

まずはXcodeで新規iOSプロジェクト作成し、cocoapods経由で、GoogleMapsGooglePlacesをインストールします。

% rbenv install 3.0.1
% rbenv local 3.0.1
% rbenv exec gem install bundler
% rbenv exec bundler -v
Bundler version 2.2.16
% cat <<EOF > Gemfile
  source "https://rubygems.org"
  gem 'rexml'
  gem 'cocoapods'
  EOF
% rbenv exec bundle install --path=vendor/bundle
% rbenv exec bundle exec pod init
% cat <<EOF > Podfile
source 'https://github.com/CocoaPods/Specs.git'
target 'your-app-name' do
  pod 'GoogleMaps', '4.2.0'
  pod 'GooglePlaces', '4.2.0'
end
% rbenv exec bundle exec pod install

プロジェクトの設定

  • TARGETS > your-app-name > BuildPhase > Link Binary With Libraries から CoreLocation.framework を追加する
  • Info.plistに以下を追加する
<dict>
	<key>NSLocationWhenInUseUsageDescription</key>
	<string>このアプリは位置情報を取得します</string>
	<key>LSApplicationQueriesSchemes</key>
	<array>
		<string>googlechromes</string>
		<string>comgooglemaps</string>
	</array>
</dict>

Swiftファイル等

  • entrypointの作成
  • AppDelegateを適用します
import SwiftUI
import GoogleMaps

let APIKey = "<YOUR-GCP-API-KEY-HERE>"

@main
struct Application: App {
    
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}
  • AppDelegateの定義
import Foundation
import GooglePlaces
import GoogleMaps

class AppDelegate: NSObject, UIApplicationDelegate {
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        GMSPlacesClient.provideAPIKey(APIKey)
        GMSServices.provideAPIKey(APIKey)
        return true
    }
 }
  • メイン画面
  • 位置情報のアクセスが許可されない場合はアラートを出してあげます
import SwiftUI
import GooglePlaces

struct ContentView: View {
    
    @State var manager = CLLocationManager()
    @State var alert = false
    
    var body: some View {
        GoogleMapsView(manager: $manager, alert: $alert).alert(isPresented: $alert) {
            Alert(title: Text("設定機能から位置情報へのアクセスを許可してください!"))
        }
    }
}
  • UIViewRepresentable を使ってSwiftUIでも使えるようにViewをラップしてあげます。
  • coordinatorクラスを定義して、CLLocationManagerの処理を委譲してあげます。
  • coordinatorクラスに操作したいmapViewのインスタンスを渡してあげて、位置情報が更新した時にmapを更新できるようにしてあげます。
  • 現在地情報が取得できたら、マップを表示し、現在地にマーカーを表示します。
import SwiftUI
import UIKit
import GoogleMaps

struct GoogleMapsView: UIViewRepresentable {
    
    @Binding var manager : CLLocationManager
    @Binding var alert : Bool
    @State var preciseLocationZoomLevel: Float = 15.0
    @State var approximateLocationZoomLevel: Float = 10.0
    
    let mapView = GMSMapView(frame: CGRect.zero)
    
    func makeCoordinator() -> Coordinator {
        return Coordinator(mapView: self)
    }
    
    /// Creates a `UIView` instance to be presented.
    func makeUIView(context: Self.Context) -> GMSMapView {
        mapView.isHidden = true
        manager.delegate = context.coordinator
        manager.desiredAccuracy = kCLLocationAccuracyBest
        manager.requestWhenInUseAuthorization()
        manager.distanceFilter = 50
        manager.startUpdatingLocation()
        return mapView
    }

    /// Updates the presented `UIView` (and coordinator) to the latest
    /// configuration.
    func updateUIView(_ mapView: GMSMapView, context: Self.Context) {
    }
    
    class Coordinator : NSObject, CLLocationManagerDelegate {
        
        var parent: GoogleMapsView
        
        init(mapView: GoogleMapsView) {
            parent = mapView
        }
        
        func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
            switch manager.authorizationStatus {
            case .authorizedAlways, .authorizedWhenInUse:
                parent.alert = false
            case .notDetermined, .denied, .restricted:
                parent.alert = true
                // show default location
                // parent.mapView.isHidden = false
                // coordinate -33.86,151.20 at zoom level 6.
                // let camera = GMSCameraPosition.camera(withLatitude: -33.86, longitude: 151.20, zoom: 6.0)
                // mapView.camera = camera
            @unknown default:
                fatalError()
            }
        }
        
        func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
            let location: CLLocation = locations.last!
            print("Location: \(location)")
            let zoomLevel = manager.accuracyAuthorization == .fullAccuracy ? parent.preciseLocationZoomLevel : parent.approximateLocationZoomLevel
            let camera = GMSCameraPosition.camera(withLatitude: location.coordinate.latitude, longitude: location.coordinate.longitude, zoom: zoomLevel)
            if parent.mapView.isHidden {
                parent.mapView.isHidden = false
                parent.mapView.camera = camera
            } else {
                parent.mapView.clear()
                let marker = GMSMarker()
                marker.position = CLLocationCoordinate2D(latitude: location.coordinate.latitude, longitude: location.coordinate.longitude)
                marker.title = "Your Place"
                marker.snippet = "Home"
                marker.map = parent.mapView
                parent.mapView.animate(to: camera)
            }
        }
        
        // Handle location manager errors.
        func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
            manager.stopUpdatingLocation()
            print("Error: \(error)")
        }
    }
}

以上です。