[Web API クライアント] : Alamofire+SwiftyJSONからURLSession+Codableへの移行

# Json型のWeb APIから情報を取得する方法

 https://apps.apple.com/us/app/neal/id1550909765

　　私の自作アプリ 「neal - 近辺飲食店検索アプリ」を制作する上で、ぐるなび株式会社が提供していたOpen APIを使用しています。ぐるなびのAPIはJson型であり、APIの処理をするために初めはAlamofireとSwiftyJsonを使用していました。しかし、そもそもこの２つのライブラリを導入する手間がかかるということや、コード全体の量が増えてしまうということが起こったので、今回は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を用いた情報の取得

```swift:GurunaviService.swift

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を用いて以下のように変更しました。

```swift:ShopData.swift
struct ShopData {
    var hit_count = Int()
    var shopNames = [String]()
    var shopCategories = [String]()
    var shopOpentimes = [String]()
    var shopMobileUrls = [String]()
    var shopLocationCoordinates = [MKPointAnnotation]()
    var shopsImages = [[String]]()
}
```

```swift:GurunaviAPIRequest.swift

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データを取得する方法にも複数の選択肢があるんですね~
まだまだ書き換えていく必要があると思いますが、これからも質の良いコードが書けるように日々頑張っていきます！！