Using Swift Concurrency to create a simple APIClient

Yoki
3 min readNov 1, 2021

--

Photo by Martin Adams on Unsplash

Swift Concurrency is attractive. To try it out, let’s create an APIClient and display the response in the View(Swift UI).

We are using the REST API from Github.
https://docs.github.com/ja/rest

The final code is here. https://github.com/yyokii/APIClientUsingSwiftConcurrency

API Request/Response

First, we need to define the settings for the API request and the type to be received in the response. We are not using the Swift Concurrency feature here.

We will use the Github API for getting user information.

Here is the response.

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

Implement the base of the API request.

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
}
}

Create an API request type to get the user information.

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

Implement APIClient.

Define errors in APIClient.

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

The async keyword is used in the send method.

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

Let’s use the APIClient we just created. The points are as follows.

  • @MainActor causes the change notification to be executed on the main thread.
  • The send method of APIClient is used and the await keyword is used to get the data.
@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)
}
}
}

Rendering

Finally, we will render. The data getting process is called at OnAppear. You can use Task.init to call asynchronous functions.

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

You can find the code here. https://github.com/yyokii/APIClientUsingSwiftConcurrency

I hope this will give you a chance to get to know Swift Concurrency.

--

--