In the fourth quarter of 2023, we built an iOS app in which the networking code, i.e., the code for making HTTP requests and receiving HTTP responses, was generated entirely using Apple's Swift OpenAPI Generator. In this blog post, we will briefly cover our experiences using the tool.
One of our goals for 2023 was to begin documenting the communication between the mobile apps and backends we build using OpenAPI – a standard for describing and documenting APIs. At the time of writing this, we have documented the communication of 10 active projects using OpenAPI.
Upon documenting the communication, we also began adopting spec-driven development – a process where both app developers and backend developers define the API contract and document it in an OpenAPI specification before beginning development. Prior to this, developers would document an API in an unstructured manner in Confluence or a similar tool and then start writing the code. Documenting the API using OpenAPI provides clearer communication of the contract. At this point, however, we had not started generating code from our OpenAPI specifications as spec-driven development describes.
During WWDC 2023, Apple's yearly worldwide conference for developers, the company announced Swift OpenAPI Generator, a tool that takes an OpenAPI specification as input and outputs the code needed to communicate between a mobile app and a backend. In other words, because we had begun documenting our APIs using OpenAPI specifications, Apple could now provide us with all the code for networking calls. You can imagine how pleased we were to hear this announcement during WWDC, and we were even more pleased to learn that the newly introduced tool generates code compatible with iOS 13 and newer. This means we can start using the tool to generate code for all new apps, as they typically support the current iOS version and one year behind, that is, iOS 17 and iOS 16 at the time of writing this.
Apple isn't the first company to build a tool that generates networking code from an OpenAPI specification, and tools like OpenAPI Generator and Microsoft's Kiota existed prior to Apple’s entry, supporting the generation of networking code for several programming languages, including Swift and Kotlin. We have only briefly looked into generating Swift code with these tools, as the announcement of Apple's Swift OpenAPI Generator coincided perfectly with the point at which our adoption of the OpenAPI specification across the company was mature enough that we could start generating networking code from our documentation.
At Shape, we consider it a huge win that the code generator is developed by Apple. This means that we avoid relying on a third-party tool and instead use a tool developed and maintained by the company for whose platform we are building apps. As is the case with other parts of the Swift ecosystem, the Swift OpenAPI Generator is open source, and its codebase is available on GitHub.
Let's have a closer look at how we want to go about integrating the generator in the app.
Our iOS app’s architecture
Our codebase is divided into feature packages, with each package being a Swift package that contains all of the business logic and UI elements for a specific feature within the app. The feature packages are further split into three targets, which consist of the data layer, the domain layer, and the UI layer of the feature. As an example, the Swift package containing the onboarding feature has a structure like this:
let package = Package(
name: "Onboarding",
platforms: [.iOS(.v16), .macOS(.v10_15)],
products: [
.library(name: "OnboardingData", targets: [
"OnboardingData"
]),
.library(name: "OnboardingDomain", targets: [
"OnboardingDomain"
]),
.library(name: "OnboardingView", targets: [
"OnboardingUI"
])
],
targets: [
.target(name: "OnboardingData", dependencies: [
"OnboardingDomain"
]),
.target(name: "OnboardingDomain"),
.target(name: "OnboardingUI", dependencies: [
"OnboardingDomain"
])
]
)
Notice that both the data layer and UI layer depend on the domain layer, while the domain layer does not have any dependencies. The UI layer is not allowed to have a dependency on the data layer, and vice versa.
The domain layer contains only simple types and interfaces, while the data layer contains implementations of those interfaces. The UI layer receives implementations from the data layer through dependency injection. For more information on how we adopt this architecture in iOS apps, please watch the recording of my talk “Achieving Loose Coupling with Pure Dependency and the Composition Root Pattern” from SwiftLeeds and Swift Connection.
With this architecture, it becomes a hard requirement for the networking code to reside in the data layer of the app. Our domain and UI layers should not need to know that data is being received over the network. Instead, we consider that as an implementation detail. The benefits of this are that we can replace the implementation with mocked data during development or testing, and in this particular case, it also aids in replacing the implementation of the networking layer, should we decide to part with Swift OpenAPI Generator in the future.
While our codebase is separated into features, our OpenAPI documentation is not. Instead, we have a single OpenAPI document that describes all endpoints in our API across all features. For small to medium-sized projects, we find it easier to maintain a single document that describes all endpoints, whereas for larger projects, the backend is usually divided into several services, with each service being documented in a separate OpenAPI document. OpenAPI viewers, such as Swagger UI, are great at structuring and visualising documentation, making large documents readable.
We decided to have the networking code reside in a separate Swift package rather than putting it in the data layer of each feature package. We named this package APIClient and let the data layer in our feature packages depend on it, as shown below.
let package = Package(
// ...
dependencies: [
.package(name: "APIClient", path: "../APIClient")
],
targets: [
.target(name: "OnboardingData", dependencies: [
"OnboardingDomain",
"APIClient"
]),
// ...
]
)
It then becomes the responsibility of the data layer within the feature package to wrap the networking code residing in the API client so that it matches the models and interfaces residing in the domain layer.
With our architecture in place, let's take a look at how we can go about creating the code that resides within the APIClient package.
Generating the networking code
To generate code with Apple’s Swift OpenAPI Generator, we add three Swift packages to our project.
- Swift OpenAPI Generator: A Swift package containing an executable named swift-openapi-generator that generates networking code from an OpenAPI specification.
- Swift OpenAPI Runtime: A Swift package containing a library named OpenURLAPIRuntime, which includes abstractions and helper functions used by the generator.
- Swift OpenAPI URLSession: A Swift package containing a library named OpenAPIURLSession, which provides a transport layer for performing network requests using URLSession.
We will start by turning our attention to the Swift OpenAPI Generator, which is a Swift package plugin that can generate code using one of two approaches.
- It can be added as a plugin to a Swift package or an Xcode project, in which case, it will generate the networking code at build time, meaning that the code does not need to be committed to version control.
- It has a command-line tool that can be manually invoked and outputs Swift source code files that can be committed to version control.
Our initial idea was to use the Swift package plugin to have the generator create the networking code at build time, as suggested in Apple’s “Generating a client in a Swift package” tutorial. However, we found that while Xcode successfully builds the app when the APIClient package uses the generator through a Swift package plugin, xcodebuild gives us the following error when building the app.
_OpenAPIGeneratorCore is only to be used by swift-openapi-generator
itself—your target should not link this library or the command
line tool directly.
The error indicates that our app is using the _OpenAPIGeneratorCore library, which is not supported. However, our app is not explicitly linking the library. The _OpenAPIGeneratorCore library appears to be implicitly linked when an app depends on a Swift package that uses the Swift OpenAPI Generator, in our case the APIClient package. We have not found a workaround for this error. If you know of a workaround, then we would love to hear from you.
Instead, we opted to use Swift OpenAPI Generator’s command-line tool to have it generate Swift source files that we commit to version control. This avoids using the plugin altogether while still depending on Swift OpenAPI Generator’s runtime libraries.
We run the generator using the following command.
swift run\
--package-path Packages/APIClient\
swift-openapi-generator generate\
--mode types --mode client\
--output-directory Packages/APIClient/Sources/APIClient\
openapi.yml
This will output the following two Swift files in the Packages/APIClient/Sources/APIClient directory.
- Types.swift: Contains all the models and protocols used by the networking code. Specifically, this contains a type named APIProtocol that specifies the interface of the client used to make networking requests.
- Client.swift: Contains a struct named Client that implements the APIProtocol and is used to perform network requests.
We find it tremendously valuable that Swift OpenAPI Generator creates the APIProtocol type as it enables our codebase to depend on an abstraction of the API client rather than a concrete implementation, and as such, we can use dependency injection to provide mocked implementations that return hardcoded data during unit tests and development.
As part of adopting spec-driven development at Shape, we have decided that our OpenAPI specifications should reside in a separate repository on GitHub, one that both our iOS, Android, and backend developers have access to and in which they modify the OpenAPI specification through pull requests.
To generate Swift code from the OpenAPI specification, we have created a GitHub Actions workflow in the repository that contains the iOS codebase. The workflow is manually invoked when needed and performs the following steps to update the networking code.
- Clone the repository that contains the OpenAPI specification. The OpenAPI specification is not committed to the repository that contains the iOS codebase.
- Run the swift-openapi-generator command-line tool to generate Swift code.
- Commit the generated Swift code to a separate branch.
- Create a pull request in the iOS repository from the newly created branch. This provides clear visibility of the changes made to the generated code and ensures that no breaking changes to the networking layer are merged before the developers are ready.
This workflow has proven to be a straightforward and efficient way to adopt spec-driven development for an iOS project, while ensuring that no generated code is unintentionally merged, leading to build-time errors or unintended side effects at runtime.
Using the generated code
With the networking code generated, we can now start to use it in our feature package. Let’s assume that we have the following protocol residing in the domain layer.
public protocol StoryService {
func stories() async throws -> [Story]
}
The protocol is straightforward: it defines a service that can return an array containing instances of Story, a simple struct residing in the domain layer too. In our data layer we implement a service that conforms to the StoryService protocol to fetch stories over the network using the generated networking code as shown below.
public struct APIStoryService: StoryService {
private let api: APIProtocol
public init(api: APIProtocol) {
self.api = api
}
public func stories() async throws -> [Story] {
try await api.fetchStories(.init())
.ok.body.json
.stories
.map(Story.init)
}
}
private extension Story {
init(_ story: Components.Schemas.Story) {
let url = URL(string: story.url)!
self.init(url: url, title: story.title)
}
}
There are two things worth pointing out in this code snippet. First and foremost, notice that the service is responsible for mapping types that were created by Swift OpenAPI Generator and are residing in the APIClient package to those in our domain layer. This makes our domain and UI layers completely decoupled from the API client. Part of this mapping is performed by the extension on Story that adds an initialiser that takes an instance of Components.Schemas.Story, which is a type residing in the Types.swift file that was created by the generator.
Secondly, the APIStoryService depends on the APIProtocol type created by Swift OpenAPI Generator. By doing this, we can inject a type into our service that returns hardcoded data, enabling us to safely unit test APIStoryService and work with mocked data during development.
With this we can now create an instance of APIStoryService and inject an instance of the generated API client as shown below.
APIStoryService(
api: Client(
serverURL: try! Servers.server1(),
transport: URLSessionTransport()
)
)
We are passing an instance of URLSessionTransport, a type provided by the OpenAPIURLSession library. This tells the networking code to use URLSession for making network requests. We also tell the API client to connect to “server 1”, the staging server specified in our OpenAPI specification as follows.
servers:
- url: 'https://staging.api.example.com'
description: Staging server
- url: 'https://api.example.com'
description: Production server
It is a bit awkward that Swift OpenAPI Generator names servers as "server 1”, "server 2” and so on, but fortunately the generated code is well-documented. As such, Xcode's auto completion will inform us that "server 1" is the staging server when creating the client.
Authorising using a middleware
A great feature of Swift OpenAPI Generator’s generated client is the option to inject middleware, a concept that enables intercepting network requests and responses. Obvious use cases for this include logging the requests and responses as well as adding authorisation headers to a request before it’s sent.
Middleware is created by conforming to the ClientMiddleware protocol and passing an instance of the middleware when creating the client. In our app, we have the following middleware that ensures requests requiring authorisation are modified to add the Authorisation header, along with an access token.
struct AuthorisationMiddleware: ClientMiddleware {
let credentialsStore: CredentialsStoring
func intercept(
_ request: HTTPRequest,
body: HTTPBody?,
baseURL: URL,
operationID: String,
next: @Sendable (HTTPRequest, HTTPBody?, URL) async throws -> (HTTPResponse, HTTPBody?)
) async throws -> (HTTPResponse, HTTPBody?) {
guard request.needsAuthorization else {
return try await next(request, body, baseURL)
}
var mutableRequest = request
let accessToken = credentialsStore.getAccessToken()
let field = HTTPField(
name: .authorization,
value: "Bearer \(accessToken)"
)
mutableRequest.headerFields.append(field)
return try await next(mutableRequest, body, baseURL)
}
}
We can then pass an instance of our middleware when creating an instance of Client as shown below.
APIStoryService(
api: Client(
serverURL: try! Servers.server1(),
transport: URLSessionTransport(),
middlewares: [
AuthorisationMiddleware(
credentialsStore: CredentialsStore()
)
]
)
)
Swift OpenAPI Generator's middleware is powerful and provides a fairly straightforward interface for adding complex logic to an app, such as retrying requests or handling errors based on the response received from a backend service.
That’s a wrap
The question that remains to be answered is: Would we want to use Swift OpenAPI Generator on other iOS projects in the future? And the answer is: Absolutely! 😃
Swift OpenAPI Generator fits well into our desired app architecture and it completes the puzzle of adopting spec-driven development across our iOS projects as it enables us to generate networking code from our OpenAPI specifications.
We are thrilled that we can now rest assured that our Swift codebases adhere to the contract defined in the OpenAPI specification, as well as save development time by avoiding the need to implement the networking layer manually.
At this point we are looking into how we can best approach the task of replacing the networking code of our existing apps with code generated by Swift OpenAPI Generator, and we will certainly use it to create the networking layer of new apps.
If you are interested in using Swift OpenAPI Generator in one of your apps, we recommend watching the “Meet Swift OpenAPI Generator” session from WWDC 23 and reading the official documentation.
Join our webinar – Refining Digital Products: User Insights Meet AI
With expert speakers, great insights, and real-world use cases, this webinar will give you some tips on how to turn unstructured data into actionable strategies using AI.
Sign up below👇