標準とswift-http-typesでAPIコール処理を書き比べてみる(Swift)

## はじめに

先日Appleから [Swift HTTP Types](https://github.com/apple/swift-http-types) が公開されました。
せっかくなので、APIコール処理を標準と書き比べてみます。

## 環境

- OS：macOS Ventura 13.4.1
- Swift：5.8
- swift-http-types：0.2.1

## 例

前回の記事と同様、APIからディズニーランドの住所を取得して構造体に格納します。
http://zipcloud.ibsnet.co.jp/api/search?zipcode=2790031

https://qiita.com/uhooi/items/c0e083f916dc516175bd

## 実装を比べてみる

標準とswift-http-typesで実装を比べてみます。

### 共通処理

両方で使う共通の処理です。

#### 構造体

取得する住所を格納する構造体です。

[前回の記事](https://qiita.com/uhooi/items/c0e083f916dc516175bd#共通処理) の構造体をほぼそのまま流用します。

```Address.swift
struct Address: Decodable {
    var results: [Result]
    
    struct Result: Decodable, Identifiable {
        let id = UUID()
        var address1: String
        var address2: String
        var address3: String
        var kana1: String
        var kana2: String
        var kana3: String
    }
}
```

`Result` を `ForEach` でループさせたいため、警告が発生しますが雑に `Identifiable` へ準拠させています。

#### UI

SwiftUIでViewを実装し、 `.task()` で住所を取得して表示します。

```ContentView.swift
import SwiftUI

struct ContentView: View {
    @State private var address: Address?
    
    var body: some View {
        ForEach(address?.results ?? []) { result in
            VStack(alignment: .leading) {
                Text(result.kana1 + result.kana2 + result.kana3)
                Text(result.address1 + result.address2 + result.address3)
            }
            .font(.title)
        }
        .task {
            await getAddress(zipCode: "2790031")            
        }
    }
    
    private func getAddress(zipCode: String) async {
        // TODO: 住所を取得して `address` に格納する
    }
```

### 標準

標準の `URLSession` を使った実装です。
前回と異なり、非同期の [data(for:)](https://developer.apple.com/documentation/foundation/urlsession/3767352-data) メソッドを使います。

```ContentView.swift
import Foundation

// ...

    private func getAddress(zipCode: String) async {
        let baseUrlString = "http://zipcloud.ibsnet.co.jp"
        let searchPath = "/api/search"
        let searchUrl = URL(string: baseUrlString + searchPath)!
        guard var components = URLComponents(url: searchUrl, resolvingAgainstBaseURL: searchUrl.baseURL != nil) else {
            return
        }
        components.queryItems = [
            .init(name: "zipcode", value: zipCode),
        ] + (components.queryItems ?? [])
        var request = URLRequest(url: components.url!)
        request.httpMethod = "GET"
        request.addValue("application/json", forHTTPHeaderField: "Content-Type")
        
        do {
            let (data, response) = try await URLSession.shared.data(for: request)
            guard let response = response as? HTTPURLResponse,
                  response.statusCode == 200 else {
                return
            }
            
            self.address = try JSONDecoder().decode(Address.self, from: data)
        } catch {
            print("Error: \(error)")            
        }
    }
```

`URLRequest` の生成部分は前回と変わりませんが、APIコール部分が非同期でスッキリ書けました。

非同期処理が完了すると、住所が表示されることも確認できました。
![スクリーンショット 2023-08-12 19.38.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138245/2bba4b3d-fe19-bb89-556f-1f5ef0d83059.png)

### Swift HTTP Types

続いてSwift HTTP Typesを使った実装です。

```swift
import HTTPTypes
import HTTPTypesFoundation

// ...

    private func getAddress(zipCode: String) async {
        let baseUrlString = "http://zipcloud.ibsnet.co.jp"
        var request = HTTPRequest(url: URL(string: baseUrlString)!)
        request.method = .get
        request.path = "/api/search?zipcode=\(zipCode)"
        request.headerFields[.contentType] = "application/json"
        
        do {
            let (data, response) = try await URLSession.shared.data(for: request)
            guard response.status == .ok else {
                return
            }
            
            self.address = try JSONDecoder().decode(Address.self, from: data)
        } catch {
            print("Error: \(error)")            
        }
    }
```

URLSessionを使う部分は標準と同様ですが、HTTP周りを型安全に扱えるのが優れています。
URLSessionの代わりに [SwiftNIO](https://github.com/apple/swift-nio) を使うこともできます。

クエリストリングを簡単に扱う方法は用意されていない [^1] ので、 `path` にそのまま含めています。

[^1]: https://github.com/apple/swift-http-types/issues/19

## おわりに

Swift HTTP Typesを使うことで、HTTP周りを型安全に扱えることがわかりました。
[Alamofire](https://github.com/Alamofire/Alamofire) のようなサードパーティ製のライブラリを使っていない場合、積極的に導入してよさそうです。

ただまだ正式リリースはされていないので、それまでは導入を待ったほうがいいかもしれません。