Json型のWeb APIから情報を取得する方法
私の自作アプリ 「neal - 近辺飲食店検索アプリ」を制作する上で、ぐるなび株式会社が提供していたOpen APIを使用しています。ぐるなびのAPIはJson型であり、APIの処理をするために初めはAlamofireとSwiftyJsonを使用していました。しかし、そもそもこの2つのライブラリを導入する手間がかかるということや、コード全体の量が増えてしまうということが起こったので、今回はAppleが公式で提供しているhttp通信関連のAPIであるURLSession、オブジェクト情報を他の形式にデータ変換してくれるプロトコルCodableを用いて汎用的なWeb API クライアントを作成しました。
- URLSession : https://developer.apple.com/documentation/foundation/urlsession
- Codable : https://developer.apple.com/documentation/swift/codable
1. AlamofireとSwiftyJSONを用いた情報の取得
struct ShopData {
var hit_count = Int()
var nameArray = [String]()
var categoryArray = [String]()
var opentimeArray = [String]()
var mobileUrlArray = [String]()
var locationCoordinatesArray = [MKPointAnnotation]()
var shopsImageArray = [[String]]()
}
struct GurunaviService {
static let shared = GurunaviService()
var freeword: String = "&freeword="
var longitude: String = "&longitude="
var latitude: String = "&latitude="
var range: String = "&range=3"
func fetchData(latitude: String, longitude: String, freeword: String, completion: @escaping(ShopData) -> Void) {
var shopData = ShopData()
var imageUrlArray = [String]()
// plistファイルにapiKeyを保存し引き出してきてます
guard let apiKey = APIKeyManager().getValue(key: "apiKey") else {
return
}
var text = "https://api.gnavi.co.jp/RestSearchAPI/v3/?keyid=\(apiKey)&hit_per_page=30" + range + latitude + longitude + freeword
let url = text.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
// Alamofireを用いてhttp通信(GET)を実行
AF.request(url as! URLConvertible, method: .get, parameters: nil, encoding: JSONEncoding.default).responseJSON { response in
switch response.result {
case .success:
let json: JSON = JSON(response.data as Any)
guard var hit_count = json["total_hit_count"].int else {
shopData.hit_count = 0
completion(shopData)
return
}
if hit_count >= 15 {
hit_count = 15
}
shopData.hit_count = hit_count
let fetchingCount = hit_count - 1
let fetchingDataMax = 0...fetchingCount
for order in fetchingDataMax {
// SwiftyJsonでわかりやすくなっている
guard let shopName = json["rest"][order]["name"].string else { return }
guard let shopCategory = json["rest"][order]["category"].string else { return }
guard let shopOpentime = json["rest"][order]["opentime"].string else { return }
guard let mobileUrl = json["rest"][order]["url"].string else { return }
guard let imageUrl1 = json["rest"][order]["image_url"]["shop_image1"].string else { return }
guard let imageUrl2 = json["rest"][order]["image_url"]["shop_image2"].string else { return }
shopData.nameArray.append(shopName)
shopData.categoryArray.append(shopCategory)
shopData.opentimeArray.append(shopOpentime)
shopData.mobileUrlArray.append(mobileUrl)
imageUrlArray.append(imageUrl1)
imageUrlArray.append(imageUrl2)
if imageUrl2 != "" {
imageUrlArray.append(imageUrl2)
}
shopData.shopsImageArray.append(imageUrlArray)
imageUrlArray.removeAll()
}
case .failure(let error):
print("DEBUG: \(error)")
break
}
completion(shopData)
}
}
初めはこのようなコードを書いていました...
簡単に説明すると、ModelとしてShopDataという構造体を定義し、Alamofireによって、http通信を実行している。そして、返ってきたJSON型のデータから必要な情報を15回程度繰り返し、ShopDataをインスタンス化した後、配列型であるプロパティに格納しているというものである。(Udemyのとある講座を参考にしました...)
Swiftを学び始めた当時はこれで納得していたのですが、今見るとエラーハンドリングがしっかり書かれていない、内容を変更しづらい、いろいろと見づらいなどなど問題点が多いように感じます。
2. URLSessionとCodableを用いた情報の取得
先程の問題を解決するために、URLSessionとCodableを用いて以下のように変更しました。
struct ShopData {
var hit_count = Int()
var shopNames = [String]()
var shopCategories = [String]()
var shopOpentimes = [String]()
var shopMobileUrls = [String]()
var shopLocationCoordinates = [MKPointAnnotation]()
var shopsImages = [[String]]()
}
import UIKit
import CoreLocation
import MapKit
class GurunaviAPIRequest {
static let shared = GurunaviAPIRequest()
func request(latitude: String, longitude: String, freeword: String, completion: @escaping(Result<ShopData?, APIError>) -> Void){
var shopData = ShopData()
guard let apiKey = APIKeyManager().getValue(key: "apiKey") else { return }
guard let url = URL(string: "https://api.gnavi.co.jp/RestSearchAPI/v3/") else { return }
// URLComponentsを使用してクエリを追加している
guard var components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { return }
components.queryItems = [
URLQueryItem(name: "keyid", value: (apiKey as! String)),
URLQueryItem(name: "hit_per_page", value: "30"),
URLQueryItem(name: "freeword", value: freeword),
URLQueryItem(name: "longitude", value: longitude),
URLQueryItem(name: "latitude", value: latitude),
URLQueryItem(name: "range", value: "3")
]
var request = URLRequest(url: components.url!)
request.httpMethod = "GET"
let task = URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) in
if let error = error {
completion(.failure(APIError.unknown(error)))
}
guard let data = data, let response = response as? HTTPURLResponse else {
completion(.failure(APIError.noResponse))
return
}
if case 200..<300 = response.statusCode {
do {
let decoder = JSONDecoder()
let shopInfo = try decoder.decode(ShopsInformation.self, from: data)
var hit_count: Int {
var count = Int(shopInfo.total_hit_count)
if count >= 15 {
count = 15
}
return count
}
shopData.hit_count = hit_count
let fetchingCount = hit_count - 1
if fetchingCount < 0 {
completion(.success(shopData))
return
}
let fetchingMaxCount = 0...fetchingCount
for order in fetchingMaxCount {
shopData.shopNames.append(String(shopInfo.rest![order].name))
shopData.shopCategories.append(String(shopInfo.rest![order].category))
shopData.shopOpentimes.append(String(shopInfo.rest![order].opentime))
shopData.shopMobileUrls.append(String(shopInfo.rest![order].url))
shopData.shopsImages.append([shopInfo.rest![order].image_url.shop_image1, shopInfo.rest![order].image_url.shop_image2])
var locationCoordinateLatitude: CLLocationDegrees {
guard let latitude = CLLocationDegrees(shopInfo.rest![order].latitude) else { return 0 }
return latitude
}
var locationCoordinateLongitude: CLLocationDegrees { guard let longitude = CLLocationDegrees(shopInfo.rest![order].longitude) else { return 0 }
return longitude
}
let annotation = MKPointAnnotation()
annotation.coordinate = CLLocationCoordinate2DMake(locationCoordinateLatitude,locationCoordinateLongitude)
annotation.title = String(shopInfo.rest![order].name)
shopData.shopLocationCoordinates.append(annotation)
}
completion(.success(shopData))
} catch let decodeError {
completion(.failure(APIError.decode(decodeError)))
}
} else {
completion(.failure(APIError.server(response.statusCode)))
}
})
task.resume()
}
}
// エラーの定義
enum APIError: Error {
case decode(Error)
case noResponse
case unknown(Error)
case server(Int)
case urlError
}
// Codable型を継承し、取得する情報を構造体として定義
struct ShopsInformation: Codable {
let total_hit_count: Int
let rest: [ShopDatabase]?
}
struct ShopDatabase: Codable {
let name: String
let latitude: String
let longitude: String
let category: String
let url: String
let opentime: String
let image_url: ImageUrl
}
struct ImageUrl: Codable {
let shop_image1: String
let shop_image2: String
}
今回はURLにクエリを追加する必要があったため、URLComponentsとURLQueryItemを使用しました。公式ドキュメントのURLを以下に載せておきます
- URLComponents : https://developer.apple.com/documentation/foundation/urlcomponents
- URLQueryItem : https://developer.apple.com/documentation/foundation/urlqueryitem
Web APIからJsonデータを取得する方法にも複数の選択肢があるんですね~
まだまだ書き換えていく必要があると思いますが、これからも質の良いコードが書けるように日々頑張っていきます!!