Generating a client in an Xcode project

This tutorial guides you through building GreetingServiceClient—an API client for a fictitious service that returns a personalized greeting.

ClientXcode.tutorial
% curl 'localhost:8080/api/greet?name=Jane'
{
  "message" : "Hello, Jane"
}

The API for the service is defined using OpenAPI and you’ll create a Swift client for this service, from scratch!

Your Xcode project will make use of the Swift OpenAPI Generator plugin to generate the code you’ll use to call this API from your existing app.

Note: If you don’t already have an existing app project, first create a new SwiftUI-based iOS app Xcode project before continuing.

(Optional) Downloading and running the server locally for testing

In the next section of the guide we will create a client for this service. In order to execute requests, you can download an example server implementation and run it locally.

  1. Clone the Git repository and change the current working directory to the nested example package.

    console
    % git clone https://github.com/apple/swift-openapi-generator
    % cd swift-openapi-generator/Examples/hello-world-vapor-server-example
    client.console.1.0.txt
  2. Build and run the service.

    console
    % git clone https://github.com/apple/swift-openapi-generator
    % cd swift-openapi-generator/Examples/hello-world-vapor-server-example
    
    % swift run HelloWorldVaporServer
    ..
    Build complete! (37.91s)
    2023-12-12T09:06:32+0100 notice codes.vapor.application : [Vapor] Server starting on http://127.0.0.1:8080
    client.console.1.1.txt
  3. While keeping the server running, in a separate Terminal window, test that the server is working by using curl from the command line.

    console
    % git clone https://github.com/apple/swift-openapi-generator
    % cd swift-openapi-generator/Examples/hello-world-vapor-server-example
    
    % swift run HelloWorldVaporServer
    ..
    Build complete! (37.91s)
    2023-12-12T09:06:32+0100 notice codes.vapor.application : [Vapor] Server starting on http://127.0.0.1:8080
    
    % curl 'localhost:8080/api/greet?name=Jane'
    {
      "message" : "Hello, Jane"
    }
    client.console.1.2.txt

Configuring your target to use the Swift OpenAPI Generator plugin

Let’s extend this app to call our GreetingService API.

We will generate the client code into your existing Xcode app target, for example called “GreetingServiceClient”. Note that you can generate the code into any target in your project, and in larger projects, it can be helpful to generate the code into a dedicated framework or library.

  1. Add the two configuration files required by the Swift OpenAPI Generator build plugin.

    The first is the OpenAPI document. Add it to to the “GreetingServiceClient” target by right-clicking on the “GreetingServiceClient” folder in the project navigator, choosing “New Empty File”, and pasting the OpenAPI document on the right.

    Sources/openapi.yaml
    openapi: '3.1.0'
    info:
      title: GreetingService
      version: 1.0.0
    servers:
      - url: https://example.com/api
        description: Example service deployment.
    paths:
      /greet:
        get:
          operationId: getGreeting
          parameters:
            - name: name
              required: false
              in: query
              description: The name used in the returned greeting.
              schema:
                type: string
          responses:
            '200':
              description: A success response with a greeting.
              content:
                application/json:
                  schema:
                    $ref: '#/components/schemas/Greeting'
    components:
      schemas:
        Greeting:
          type: object
          properties:
            message:
              type: string
          required:
            - message
    client.openapi.yaml
  2. If you launched a local server in the previous section, add a localhost server entry to the OpenAPI document.

    This will make it easy to call the local server from generated code in the next section.

    Sources/openapi.yaml
    openapi: '3.1.0'
    info:
      title: GreetingService
      version: 1.0.0
    servers:
      - url: https://example.com/api
        description: Example service deployment.
      - url: http://127.0.0.1:8080/api
        description: Localhost deployment.
    paths:
      /greet:
        get:
          operationId: getGreeting
          parameters:
            - name: name
              required: false
              in: query
              description: The name used in the returned greeting.
              schema:
                type: string
          responses:
            '200':
              description: A success response with a greeting.
              content:
                application/json:
                  schema:
                    $ref: '#/components/schemas/Greeting'
    components:
      schemas:
        Greeting:
          type: object
          properties:
            message:
              type: string
          required:
            - message
    client.openapi.2.yaml
  3. The second file to add is a configuration file that controls the behavior of the build plugin. Create a file in the same target called openapi-generator-config.yaml, with the following contents.

    Sources/openapi-generator-config.yaml
    generate:
      - types
      - client
    client.openapi-generator-config.yaml
  4. Include the two files in the target by going to the Build Phases tab of the “GreetingServiceClient” target in the Project Editor, and adding the two files to the “Compile Sources” section.

    If you skip this step, you will see the error “Issues with required files: No config file found in the target…”.

  5. With the configuration files in place, we will add the following three package dependencies: the build plugin, the Runtime library, and a concrete client transport that uses URLSession to send HTTP requests.

    Select the project in the Project Navigator, select the project in the Project Editor, and go to Package Dependencies.

  6. Under Packages, click the plus button to add a new package dependency.

  7. Find the swift-openapi-generator package in an existing collection, or type in the full URL to the search field at the top: https://github.com/apple/swift-openapi-generator.

  8. Since the package provides a build plugin that we will integrate later, make sure that on the Choose Package Products screen, the “Add to Target” value is “None” for all products listed.

    Click Add Package.

  9. Repeat the same steps two more times, with the packages https://github.com/apple/swift-openapi-runtime and https://github.com/apple/swift-openapi-urlsession.

    This time, ensure the library products are added to the GreetingServiceClient target. Note, this might not be the default target Xcode offers to add the libraries to.

  10. To finish configuring the build plugin in your target, navigate to the Build Phases tab of the GreetingServiceClient in the Project Editor, and expand the Run Build Tool Plug-ins section.

    Click the plus button and add the OpenAPIGenerator plugin.

  11. To verify everything is configured correctly, choose Product -> Build. If this is the first time using the plugin, you will encounter a build error with the message "OpenAPIGenerator" is disabled. To continue, click on the error in the Issue Navigator, click “Trust & Enable”, and choose Product -> Build again.

    Xcode now builds the Swift OpenAPI Generator plugin itself, and then runs it on the configuration files openapi.yaml and openapi-generator-config.yaml to generate a Swift client for GreetingService. Once it finishes, the Client type will become available in the GreetingServiceClient target.

Using the generated code in your target

Now we’re ready to use the code that the plugin generated behind the scenes to fetch some personalized greetings!

  1. Create a new Swift file in the GreetingServiceClient app target called GreetingClient.swift.

    Import the OpenAPIURLSession library, which provides a transport implementation that uses Foundation’s URLSession to perform network calls.

    GreetingClient.swift
    import OpenAPIURLSession
    client.xcode.0.swift
  2. Define a new struct called GreetingClient with an initializer and an empty method that will fetch the greeting using the generated client.

    GreetingClient.swift
    import OpenAPIURLSession
    
    public struct GreetingClient {
    
        public init() {}
    
        public func getGreeting(name: String?) async throws -> String {
    
        }
    }
    client.xcode.1.swift
  3. Next we’ll create an instance of the generated client.

    Note: Servers.Server2.url() is the localhost service, defined in the OpenAPI document.

    GreetingClient.swift
    import OpenAPIURLSession
    
    public struct GreetingClient {
    
        public init() {}
    
        public func getGreeting(name: String?) async throws -> String {
            let client = Client(
                serverURL: try Servers.Server2.url(),
                transport: URLSessionTransport()
            )
        }
    }
    client.xcode.2.swift
  4. Finally, we can use the client to make a request and get a response.

    GreetingClient.swift
    import OpenAPIURLSession
    
    public struct GreetingClient {
    
        public init() {}
    
        public func getGreeting(name: String?) async throws -> String {
            let client = Client(
                serverURL: try Servers.Server2.url(),
                transport: URLSessionTransport()
            )
            let response = try await client.getGreeting(query: .init(name: name))
        }
    }
    client.xcode.3.swift
  5. Add a switch statement to handle the different possible responses from the server.

    Something’s missing here, and if you recompile your project you’ll see that the compiler helpfully tells you that your switch statement didn’t cover all scenarios.

    GreetingClient.swift
    import OpenAPIURLSession
    
    public struct GreetingClient {
    
        public init() {}
    
        public func getGreeting(name: String?) async throws -> String {
            let client = Client(
                serverURL: try Servers.Server2.url(),
                transport: URLSessionTransport()
            )
            let response = try await client.getGreeting(query: .init(name: name))
            switch response {
            case .ok(let okResponse):
                print(okResponse)
            }
        }
    }
    client.xcode.4.swift
  6. In the event the server provides a response that doesn’t conform to the API specification, you still have an opportunity as a client to handle it gracefully. We’ll do so, by returning a default greeting that includes the unexpected status code, indicating that our client doesn’t know what to do with this because it hasn’t been updated to handle this kind of response.

    GreetingClient.swift
    import OpenAPIURLSession
    
    public struct GreetingClient {
    
        public init() {}
    
        public func getGreeting(name: String?) async throws -> String {
            let client = Client(
                serverURL: try Servers.Server2.url(),
                transport: URLSessionTransport()
            )
            let response = try await client.getGreeting(query: .init(name: name))
            switch response {
            case .ok(let okResponse):
                print(okResponse)
            case .undocumented(statusCode: let statusCode, _):
                return "🙉 \(statusCode)"
            }
        }
    }
    client.xcode.5.swift
  7. Let’s extract and return the content from the response body.

    The switch statement over the body allows you to handle the different content types that are specified for the API operation.

    GreetingClient.swift
    import OpenAPIURLSession
    
    public struct GreetingClient {
    
        public init() {}
    
        public func getGreeting(name: String?) async throws -> String {
            let client = Client(
                serverURL: try Servers.Server2.url(),
                transport: URLSessionTransport()
            )
            let response = try await client.getGreeting(query: .init(name: name))
            switch response {
            case .ok(let okResponse):
                switch okResponse.body {
                case .json(let greeting):
                    return greeting.message
                }
            case .undocumented(statusCode: let statusCode, _):
                return "🙉 \(statusCode)"
            }
        }
    }
    client.xcode.6.swift
  8. Alternatively, if you don’t need to handle all the responses and content types exhaustively, you can use the shorthand API to unwrap the received body value.

    Note that if the actual received response or content type is different to your requested one, the unwrapping getters will thrown an error.

    GreetingClient.swift
    import OpenAPIURLSession
    
    public struct GreetingClient {
    
        public init() {}
    
        public func getGreeting(name: String?) async throws -> String {
            let client = Client(
                serverURL: try Servers.Server2.url(),
                transport: URLSessionTransport()
            )
            let response = try await client.getGreeting(query: .init(name: name))
            return try response.ok.body.json.message
        }
    }
    client.xcode.6.2.swift
  9. Finally, in your app target, integrate the client to fetch the personalized greeting, for example to show it in the UI.

    App.swift
    let greeting = try await GreetingClient().getGreeting(name: "App")
    // Display the greeting text in the UI.
    client.xcode.7.swift