0
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-04-02

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

  私の自作アプリ 「neal - 近辺飲食店検索アプリ」を制作する上で、ぐるなび株式会社が提供していたOpen APIを使用しています。ぐるなびのAPIはJson型であり、APIの処理をするために初めはAlamofireとSwiftyJsonを使用していました。しかし、そもそもこの2つのライブラリを導入する手間がかかるということや、コード全体の量が増えてしまうということが起こったので、今回はAppleが公式で提供しているhttp通信関連のAPIであるURLSession、オブジェクト情報を他の形式にデータ変換してくれるプロトコルCodableを用いて汎用的なWeb API クライアントを作成しました。

1. AlamofireとSwiftyJSONを用いた情報の取得

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

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]]()
}
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を以下に載せておきます

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

0
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?