はじめに
今回はPlaces API for iOS とGoogle Places API を使い分けた話です。
Google Places API
はデバイスやOSに依らず用意されたAPIですが、 Places API for iOS
はネーミングからも分かる通り iOS
のために用意された API
という位置づけになります。
通常であれば、「 Places API for iOS
を使えば良いのでは?」と思われるかもしれませんが、
筆者がプロダクト開発時に実現したかった内容がたまたまそぐわなかったので Google Places API
を利用するに至ったわけです。
では早速見ていきましょう。
周辺の場所情報を取得する
現在の場所を取得するのであれば、Googleがスタートガイド で説明しているように、
GMSPlacesClient
の currentPlaceWithCallback
メソッドを利用すれば可能です。
しかし、特定の場所を起点に周辺の場所情報を取得する場合は、Google Places API
を利用する方が汎用性があります。
Places API for iOS
にも Place Picker
が用意されていますが、以下理由より用途が限定的に感じられました。
UI/UXが限定されている
特定の属性のものだけ検索することに向いていない
そこで筆者は、「周辺の病院を検索する」ケースでは Google Places API
を利用することにしました。
以下に具体的な実装を記載します。
通信ライブラリとして Moya
を非同期処理のために PromiseKit
を利用します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
// HospitalAPI.swift
import Foundation
import Moya
import PromiseKit
import GooglePlaces
internal enum HospitalAPITarget {
case hospitals ( lat: Double , lng: Double )
}
internal enum APIError: Error {
case cancel
case apiError ( description: String )
case decodeError
}
internal enum GooglePlacesError: Error {
case cancel
case notFoundError
}
extension HospitalAPITarget: TargetType {
/// API Key
private var apiKey: String {
guard let path = Bundle . main . path ( forResource: "key" , ofType: "plist" ) else {
fatalError ( "key.plistが見つかりません" )
}
guard let dic = NSDictionary ( contentsOfFile: path ) as ? [ String: Any ] else {
fatalError ( "key.plistの中身が想定通りではありません" )
}
guard let apiKey = dic [ "googleApiKey" ] as ? String else {
fatalError ( "Google APIのKeyが設定されていません" )
}
return apiKey
}
// ベースURLを文字列で定義
private var _baseURL: String {
switch self {
case . hospitals:
return R . string . url . googlePlacesApiPlaceUrl ()
}
}
public var baseURL: URL {
return URL ( string: _baseURL ) !
}
// enumの値に対応したパスを指定
public var path: String {
switch self {
case . hospitals:
return ""
}
}
// enumの値に対応したHTTPメソッドを指定
public var method: Moya . Method {
switch self {
case . hospitals:
return . get
}
}
// スタブデータの設定
public var sampleData: Data {
switch self {
case . hospitals:
return "Stub data" . data ( using: String . Encoding . utf8 ) !
}
}
// パラメータの設定
var task: Task {
switch self {
case let . hospitals ( lat , lng ) :
return . requestParameters ( parameters: [
R . string . common . keyFileName () : apiKey ,
R . string . common . locationKeyName () : "\(lat),\(lng)" ,
R . string . common . radiusKeyName () : 1500 ,
R . string . common . typeKeyName () : "hospital"
], encoding: URLEncoding . default )
}
}
// ヘッダーの設定
var headers: [ String: String ] ? {
switch self {
case . hospitals:
return nil
}
}
}
class HospitalAPI: HospitalProtocol {
private var provider: MoyaProvider < HospitalAPITarget >!
/// イニシャライザ
init () {
provider = MoyaProvider < HospitalAPITarget > ()
}
// MARK: CRUD operations
/// 指定の緯度、経度から一定範囲内の病院を検索する処理
///
/// - Returns: 病院のプレイス情報
func fetchHospitals ( lat: Double , lng: Double ) -> Promise < [ Place ] > {
let ( promise , resolver ) = Promise < [ Place ] > . pending ()
provider . request (. hospitals ( lat: lat , lng: lng )) { result in
switch result {
case . success ( let response ) :
do {
let decoder = JSONDecoder ()
decoder . keyDecodingStrategy = . convertFromSnakeCase
let places = try decoder . decode ( Places . self , from: response . data )
resolver . fulfill ( places . results )
} catch {
resolver . reject ( APIError . decodeError )
}
case . failure ( let error ) :
resolver . reject ( APIError . apiError ( description: error . localizedDescription ))
}
}
return promise
}
}
因みに、上記 fetchHospitals
メソッド内で利用している Place
の定義は以下です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// Place.swift
import Foundation
public struct Location: Codable {
public var lat: Double
public var lng: Double
}
public struct Viewport: Codable {
public var northeast: Location
public var southwest: Location
}
public struct Geometry: Codable {
public var viewport: Viewport
public var location: Location
}
public struct OpeningHours: Codable {
public var weekdayText: [ String ]
public var openNow: Bool
}
public struct Photos: Codable {
public var photoReference: String
public var width: Double
public var height: Double
public var htmlAttributions: [ String ]
}
public struct Place: Codable {
public var id : String
public var placeId: String
public var name: String
public var icon: String
public var rating: Double ?
public var scope: String
public var vicinity: String
public var reference: String
public var priceLevel: Int ?
public var types: [ String ]
public var geometry: Geometry
public var openingHours: OpeningHours ?
public var photos: [ Photos ] ?
}
public struct Places: Codable {
public var results: [ Place ]
public var status: String
public var htmlAttributions: [ String ]
}
場所の画像を取得する
Webクライアントから場所の画像を取得する場合は先程APIを叩いて取得した結果の photos
からAPIを叩く流れになります。
しかしながら、iOSでは画像取得用のメソッドが用意されています。
このメソッドは プレイスID
さえ指定すれば簡単に取得できるため、
ここは適材適所で Places API for iOS
の lookUpPhoto
および loadPlacePhoto
を利用した方が良いでしょう。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// HospitalAPI.swift
func fetchPhoto ( placeId: String ) -> Promise < UIImage ?> {
let ( promise , resolver ) = Promise < UIImage ?> . pending ()
GMSPlacesClient . shared (). lookUpPhotos ( forPlaceID: placeId ) { ( photos , error ) in
if let error = error {
resolver . reject ( error )
return
}
guard let firstPhoto = photos ? . results . first else {
resolver . reject ( GooglePlacesError . notFoundError )
return
}
GMSPlacesClient . shared (). loadPlacePhoto ( firstPhoto , callback: { ( image , error ) in
if let error = error {
resolver . reject ( error )
return
}
resolver . fulfill ( image )
})
}
return promise
}
まとめ
今回筆者が改めて学んだのは、iOSアプリを開発するからといって、Places API for iOS
だけに焦点を絞るのではなく、
『結局、何がしたくて、そのためにどういった情報を取得したいのか』
をきちんど整理した上で適切な手段を用いるということです。
Googleが提供しているAPIは勉強になりますね。
と言ったところで本日はここまで。