11
4

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.

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

Posted at

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を利用します。

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

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

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

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"
}
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リクエストの型を作成します。

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のエラーを定義します。

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

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

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キーワードを利用してデータを取得しています。
@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を利用し、非同期関数を呼び出すことができます。

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に触れるきっかけになれば幸いです。

11
4
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
11
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?