Say Hello to Swift OpenAPI Generator
A Swift Package plugin to help you simplify your codebase
In this week's post, we will take a look at the Swift OpenAPI Generator, a Swift Package plugin that can help us consume HTTP APIs in iOS apps. If you are not familiar with OpenAPI, it is a widely adopted industry standard. Its conventions and best practices help you work with APIs. For example, this is the standard we use to describe our own REST API.
With OpenAPI, you document the API service in either YAML or JSON. These machine-readable formats allow you to benefit from a rich ecosystem of tooling. There's tools for test generation, runtime validation, interoperability, and in this case, for code generation.
Basically, instead of writing our API request manually using something like URLSession
, which requires some code in order to handle our requests and responses, we will use Swift OpenAPI Generator to simplify this process.
For this post, we will use a small API we've written that generates random quotes. For sake of simplicity, it contains one single endpoint and every time it is invoked, it generates a new quote:
Its OpenAPI specification file looks like this:
openapi: "3.0.3"
info:
title: QuoteGenerator
version: 1.0.0
servers:
- url: http://localhost:3000/api
description: "My quotes API"
paths:
/quote:
get:
operationId: getQuote
responses:
'200':
description: "Returns a random quote"
content:
text/plain:
schema:
type: string
In this post, we will not explore more complex APIs, instead we'll simply focus on how Swift OpenAPI Generator can simplify our codebase by generating the code needed to consume an API. This Swift Package runs at build time, and the code generated is always in sync with the OpenAPI document and doesn't need to be committed to a source repository.
For this post, we've created a simple app, basically just a single button that, when clicked, will retrieve a quote for us:
But let's start by adding the swift-openapi-generator which provides the Swift Package plugin, the swift-openapi-runtime which provides the common types and abstractions used by the generated code and the swift-openapi-urlsession as our integration package to allow us to use URLSession
in our iOS app.
You should add these as dependencies in your app's project, under the Package Dependencies tab:
With the dependencies in place, we can configure the target to use the OpenAPI Generator plugin. You can do this in the app's target, by expanding the Run Build Tool Plug-ins section:
We can then add both the openapi.yaml
:
openapi: "3.0.3"
info:
title: QuoteGenerator
version: 1.0.0
servers:
- url: http://localhost:3000/api
description: "My quotes API"
paths:
/quote:
get:
operationId: getQuote
responses:
'200':
description: "Returns a random quote"
content:
text/plain:
schema:
type: string
And openapi-generator-config.yaml
:
generate:
- types
- client
To your Xcode project:
The plugin will use these two files to generate all the necessary code.
Let's switch back to ContentView.swift
, which will recompile our project so the generated code is ready to use in our app.
As a security measure, you'll be asked to trust the plugin the first time you use it.
You can now, add the following code in the ContentView.swift
:
import SwiftUI
import OpenAPIRuntime
import OpenAPIURLSession
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct ContentView: View {
@State private var quote = ""
var body: some View {
VStack {
Text(quote).font(.system(size: 30)).multilineTextAlignment(.center)
Button("Get Quote") {
Task { try? await updateQuote() }
}
}
.padding()
.buttonStyle(.borderedProminent)
}
let client: Client
init() {
self.client = Client(
serverURL: try! Servers.server1(),
transport: URLSessionTransport()
)
}
func updateQuote() async throws {
let response = try await client.getQuote(Operations.getQuote.Input())
switch response {
case let .ok(okResponse):
switch okResponse.body {
case .text(let text):
quote = text
}
case .undocumented(statusCode: let statusCode, _):
print("Error: \(statusCode)")
quote = "Failed to get quote!"
}
}
}
Let's go by parts, the view will contain a very basic UI, simply a text (that will use a state property) and a button (that will invoke the HTTP request):
struct ContentView: View {
@State private var quote = ""
var body: some View {
VStack {
Text(quote).font(.system(size: 30)).multilineTextAlignment(.center)
Button("Get Quote") {
Task { try? await updateQuote() }
}
}
.padding()
.buttonStyle(.borderedProminent)
}
...more code
}
We will then use the generated code which provides a client property. We will use it in our view with an initializer which configures it to use our API:
struct ContentView: View {
...more code
let client: Client
init() {
self.client = Client(
serverURL: try! Servers.server1(),
transport: URLSessionTransport()
)
}
...more code
And finally, we add the function that makes the API call to the server using that client property:
struct ContentView: View {
...more code
func updateQuote() async throws {
let response = try await client.getQuote(Operations.getQuote.Input())
switch response {
case let .ok(okResponse):
switch okResponse.body {
case .text(let text):
quote = text
}
case .undocumented(statusCode: let statusCode, _):
print("Error: \(statusCode)")
quote = "Failed to get quote!"
}
}
...more code
}
You can handle the response, if successful, and retrieve our quote, or in case the server responds with something that isn't specified in its OpenAPI document, you still have a chance to handle that gracefully.
As you can see, the generated code substantially simplifies how you use URLSession
to consume an API.
And that's basically it, every time we tap the button, it will retrieve a new quote from our API, and display it in the text component:
Cool, right?
Although this is just a simple example, we hope this post helped you understand the benefits of using OpenAPI specification files to help eliminate ambiguity and enable spec-driven development in your iOS apps.
As always, we hope you liked this article, and if you have anything to add, we are available via our Support Channel.