【Swift】Swift Concurrency を使って簡易なAPIClientを作ってみる

Swift Concurrency は魅力的です。
それを試しに使ってみるために、APIClientを作成しレスポンスをViewに表示してみます（SwiftUIを利用）。

GithubのREST APIを利用しています。
https://docs.github.com/ja/rest

最終的なコードはこちらにあります。
https://github.com/yyokii/APIClientUsingSwiftConcurrency

## APIリクエスト/レスポンス

まず、APIリクエストする際の設定とレスポンスで受け取る型を定義します。
ここではSwift Concurrencyの機能は利用していません。

今回はGithubのユーザー情報取得のAPIを利用します。

レスポンスはこちらです。

```swift
public struct UserInformation: Codable {
    public var login: String
    public var id: Int
    public var followers: Int
    public var following: Int
}
```

APIリクエストのベースとなる実装をします。

```swift
public enum HTTPMethod: String {
    case connect = "CONNECT"
    case delete = "DELETE"
    case get = "GET"
    case head = "HEAD"
    case options = "OPTIONS"
    case patch = "PATCH"
    case post = "POST"
    case put = "PUT"
    case trace = "TRACE"
}
```

```swift
public protocol GithubAPIBaseRequest {
    associatedtype Response: Decodable
    
    var baseURL: URL { get }
    var method: HTTPMethod { get }
    var path: String { get }
    var body: String { get }
    var queryItems: [URLQueryItem] { get }

    func buildURLRequest() -> URLRequest
}

extension GithubAPIBaseRequest {
    public func buildURLRequest() -> URLRequest {
        let url = baseURL.appendingPathComponent(path)
        
        var components = URLComponents(url: url, resolvingAgainstBaseURL: true)
        
        switch method {
        case .get:
            components?.queryItems = queryItems
        default:
            fatalError()
        }
        
        var urlRequest = URLRequest(url: url)
        urlRequest.url = components?.url
        urlRequest.httpMethod = method.rawValue
                
        return urlRequest
    }
}
```

ユーザー情報を取得するAPIリクエストの型を作成します。

```swift
let baseURLString: String = "https://api.github.com"

/// Get a user
public struct UserInformationRequest: GithubAPIBaseRequest {
    public typealias Response = UserInformation
    
    public var baseURL: URL = URL(string: baseURLString)!
    public var path: String = "users"
    public var method: HTTPMethod = .get
    public var body: String = ""
    public var queryItems: [URLQueryItem] = []
    
    public init(userName: String) {
        path += "/\(userName)"
    }
}
```

## APIClient

APIClientを実装します。

APIClientのエラーを定義します。

```swift
enum GithubAPIClientError: Error {
    case connectionError(Data)
    case apiError
}
```

やっとAPIClientです。
`send`メソッドにて`async`キーワードが登場しています。

```swift
public struct GithubAPIClient {
    private let session: URLSession = {
        let config = URLSessionConfiguration.default
        return URLSession(configuration: config)
    }()
    
    private let decoder: JSONDecoder = {
        let decoder = JSONDecoder()
        decoder.keyDecodingStrategy = .convertFromSnakeCase
        return decoder
    }()
    
    public init() {}
    
    public func send<Request: GithubAPIBaseRequest>(request: Request) async throws -> Request.Response {
        
        let result = try await session.data(for: request.buildURLRequest())
        try validate(data: result.0, response: result.1)
        return try decoder.decode(Request.Response.self, from: result.0)
    }
    
    func validate(data: Data, response: URLResponse) throws {
        guard let code = (response as? HTTPURLResponse)?.statusCode else {
            throw GithubAPIClientError.connectionError(data)
        }
        
        guard (200..<300).contains(code) else {
            throw GithubAPIClientError.apiError
        }
    }
}
```

## ViewModel

先ほど作ったAPIClientを利用してみます。
ポイントは以下です。

* `@MainActor`により、変更通知がメインスレッド上で実行されます。
* APIClientの`send`メソッドを利用し、`await`キーワードを利用してデータを取得しています。

```swift
@MainActor
final class HomeViewModel: ObservableObject {
    
    @Published var id: Int = -1
    @Published var login: String = ""
    @Published var followers: Int = 0
    @Published var following: Int = 0
    
    let client = GithubAPIClient()
    
    init() {}
    
    func fetchChrisInfo() async {
        let req = UserInformationRequest(userName: "defunkt")
        
        do {
            let data: UserInformation = try await client.send(request: req)
            self.id = data.id
            self.login = data.login
            self.followers = data.followers
            self.following = data.following
        } catch {
            print(error)
        }
    }
}
```

## 描画

最後に描画です。
`OnAppear`にて、データ取得処理を呼び出しています。
`Task.init`を利用し、非同期関数を呼び出すことができます。

```swift
public struct HomeView: View {
    @StateObject private var vm: HomeViewModel = HomeViewModel()

    public init() {}
    
    public var body: some View {
        VStack(spacing: 20) {
            Text("ID: \(vm.id)")
            Text("Login: \(vm.login)")
            Text("Followers: \(vm.followers)")
            Text("Following: \(vm.following)")
        }
        .onAppear {
            Task {
                await vm.fetchChrisInfo()
            }
        }
    }
}
```

## Conclusion

コードはこちらにあります。
https://github.com/yyokii/APIClientUsingSwiftConcurrency

いかがでしたでしょうか？
Swift Concurrencyに触れるきっかけになれば幸いです。