Dunfey · Hotel WWDC as data, est. 1983
Front desk everything
Years
Topics

2023 Developer ToolsEssentialsSwift

WWDC23 · 22 min · Developer Tools / Essentials / Swift

Meet Swift OpenAPI Generator

Discover how Swift OpenAPI Generator can help you work with HTTP server APIs whether you’re extending an iOS app or writing a server in Swift. We’ll show you how this package plugin can streamline your workflow and simplify your codebase by generating code from an OpenAPI document.

Watch at developer.apple.com ↗

Transcript all transcripts

Chapters

  • 0:44 — Considerations when making API calls
  • 1:52 — Meet OpenAPI
  • 6:15 — Making API calls from your app
  • 12:33 — Adapting as the API evolves
  • 14:23 — Testing your app with mocks
  • 16:12 — Server development in Swift
  • 19:24 — Adding a new operation

Code shown on screen · 12 snippets

Example OpenAPI document yaml · at 4:17 ↗
openapi: "3.0.3"
info:
  title: "GreetingService"
  version: "1.0.0"
servers:
- url: "http://localhost:8080/api"
  description: "Production"
paths:
  /greet:
    get:
      operationId: "getGreeting"
      parameters:
      - name: "name"
        required: false
        in: "query"
        description: "Personalizes the greeting."
        schema:
          type: "string"
      responses:
        "200":
          description: "Returns a greeting"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Greeting"
CatService openapi.yaml yaml · at 7:05 ↗
openapi: "3.0.3"
info:
  title: CatService
  version: 1.0.0
servers:
  - url: http://localhost:8080/api
    description: "Localhost cats 🙀"
paths:
  /emoji:
    get:
      operationId: getEmoji
      parameters:
      - name: count
        required: false
        in: query
        description: "The number of cats to return. 😽😽😽"
        schema:
          type: integer
      responses:
        '200':
          description: "Returns a random emoji, of a cat, ofc! 😻"
          content:
            text/plain:
              schema:
                type: string
Making API calls from your app swift · at 8:03 ↗
import SwiftUI
import OpenAPIRuntime
import OpenAPIURLSession

#Preview {
    ContentView()
}

struct ContentView: View {
    @State private var emoji = "🫥"

    var body: some View {
        VStack {
            Text(emoji).font(.system(size: 100))
            Button("Get cat!") {
                Task { try? await updateEmoji() }
            }
        }
        .padding()
        .buttonStyle(.borderedProminent)
    }

    let client: Client

    init() {
        self.client = Client(
            serverURL: try! Servers.server1(),
            transport: URLSessionTransport()
        )
    }

    func updateEmoji() async throws {
        let response = try await client.getEmoji(Operations.getEmoji.Input())

        switch response {
        case let .ok(okResponse):
            switch okResponse.body {
            case .text(let text):
                emoji = text
            }
        case .undocumented(statusCode: let statusCode, _):
            print("cat-astrophe: \(statusCode)")
            emoji = "🙉"
        }
    }
}
CatServiceClient openapi-generator-config.yaml yaml · at 9:48 ↗
generate:
  - types
  - client
Adapting as the API evolves swift · at 13:24 ↗
import SwiftUI
import OpenAPIRuntime
import OpenAPIURLSession

#Preview {
    ContentView()
}

struct ContentView: View {
    @State private var emoji = "🫥"

    var body: some View {
        VStack {
            Text(emoji).font(.system(size: 100))
            Button("Get cat!") {
                Task { try? await updateEmoji() }
            }
            Button("More cats!") {
                Task { try? await updateEmoji(count: 3) }
            }
        }
        .padding()
        .buttonStyle(.borderedProminent)
    }

    let client: Client

    init() {
        self.client = Client(
            serverURL: try! Servers.server1(),
            transport: URLSessionTransport()
        )
    }

    func updateEmoji(count: Int = 1) async throws {
        let response = try await client.getEmoji(Operations.getEmoji.Input(
            query: Operations.getEmoji.Input.Query(count: count)
        ))

        switch response {
        case let .ok(okResponse):
            switch okResponse.body {
            case .text(let text):
                emoji = text
            }
        case .undocumented(statusCode: let statusCode, _):
            print("cat-astrophe: \(statusCode)")
            emoji = "🙉"
        }
    }
}
Testing your app with mocks swift · at 15:06 ↗
import SwiftUI
import OpenAPIRuntime
import OpenAPIURLSession

#Preview {
    ContentView(client: MockClient())
}

struct ContentView<C: APIProtocol>: View {
    @State private var emoji = "🫥"

    var body: some View {
        VStack {
            Text(emoji).font(.system(size: 100))
            Button("Get cat!") {
                Task { try? await updateEmoji() }
            }
            Button("More cats!") {
                Task { try? await updateEmoji(count: 3) }
            }
        }
        .padding()
        .buttonStyle(.borderedProminent)
    }

    let client: C

    init(client: C) {
        self.client = client
    }

    init() where C == Client {
        self.client = Client(
            serverURL: try! Servers.server1(),
            transport: URLSessionTransport()
        )
    }

    func updateEmoji(count: Int = 1) async throws {
        let response = try await client.getEmoji(Operations.getEmoji.Input(
            query: Operations.getEmoji.Input.Query(count: count)
        ))

        switch response {
        case let .ok(okResponse):
            switch okResponse.body {
            case .text(let text):
                emoji = text
            }
        case .undocumented(statusCode: let statusCode, _):
            print("cat-astrophe: \(statusCode)")
            emoji = "🙉"
        }
    }
}

struct MockClient: APIProtocol {
    func getEmoji(_ input: Operations.getEmoji.Input) async throws -> Operations.getEmoji.Output {
        let count = input.query.count ?? 1
        let emojis = String(repeating: "🤖", count: count)
        return .ok(Operations.getEmoji.Output.Ok(
            body: .text(emojis)
        ))
    }
}
Implementing a backend server swift · at 16:58 ↗
import Foundation
import OpenAPIRuntime
import OpenAPIVapor
import Vapor

struct Handler: APIProtocol {
    func getEmoji(_ input: Operations.getEmoji.Input) async throws -> Operations.getEmoji.Output {
        let candidates = "🐱😹😻🙀😿😽😸😺😾😼"
        let chosen = String(candidates.randomElement()!)
        let count = input.query.count ?? 1
        let emojis = String(repeating: chosen, count: count)
        return .ok(Operations.getEmoji.Output.Ok(body: .text(emojis)))
    }
}

@main
struct CatService {
    public static func main() throws {
        let app = Vapor.Application()
        let transport = VaporTransport(routesBuilder: app)
        let handler = Handler()
        try handler.registerHandlers(on: transport, serverURL: Servers.server1())
        try app.run()
    }
}
CatService Package.swift swift · at 18:43 ↗
// swift-tools-version: 5.8
import PackageDescription

let package = Package(
    name: "CatService",
    platforms: [
        .macOS(.v13),
    ],
    dependencies: [
        .package(
             url: "https://github.com/apple/swift-openapi-generator",
            .upToNextMinor(from: "0.1.0")
        ),
        .package(
            url: "https://github.com/apple/swift-openapi-runtime",
            .upToNextMinor(from: "0.1.0")
        ),
        .package(
            url: "https://github.com/swift-server/swift-openapi-vapor",
            .upToNextMinor(from: "0.1.0")
        ),
        .package(
            url: "https://github.com/vapor/vapor",
            .upToNextMajor(from: "4.69.2")
        ),
    ],
    targets: [
        .executableTarget(
            name: "CatService",
            dependencies: [
                .product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"),
                .product(name: "OpenAPIVapor", package: "swift-openapi-vapor"),
                .product(name: "Vapor", package: "vapor"),
            ],
            resources: [
                .process("Resources/cat.mp4"),
            ],
            plugins: [
                .plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator"),
            ]
        ),
    ]
)
CatService openapi.yaml yaml · at 19:08 ↗
openapi: "3.0.3"
info:
  title: CatService
  version: 1.0.0
servers:
  - url: http://localhost:8080/api
    description: "Localhost cats 🙀"
paths:
  /emoji:
    get:
      operationId: getEmoji
      parameters:
      - name: count
        required: false
        in: query
        description: "The number of cats to return. 😽😽😽"
        schema:
          type: integer
      responses:
        '200':
          description: "Returns a random emoji, of a cat, ofc! 😻"
          content:
            text/plain:
              schema:
                type: string
CatService openapi-generator-config.yaml yaml · at 19:10 ↗
generate:
  - types
  - server
Adding an operation to the OpenAPI document yaml · at 20:11 ↗
openapi: "3.0.3"
info:
  title: CatService
  version: 1.0.0
servers:
  - url: http://localhost:8080/api
    description: "Localhost cats 🙀"
paths:
  /emoji:
    get:
      operationId: getEmoji
      parameters:
      - name: count
        required: false
        in: query
        description: "The number of cats to return. 😽😽😽"
        schema:
          type: integer
      responses:
        '200':
          description: "Returns a random emoji, of a cat, ofc! 😻"
          content:
            text/plain:
              schema:
                type: string

  /clip:
    get:
      operationId: getClip
      responses:
        '200':
          description: "Returns a cat video! 😽"
          content:
            video/mp4:
              schema:
                type: string
                format: binary
Adding a new API operation swift · at 20:26 ↗
import Foundation
import OpenAPIRuntime
import OpenAPIVapor
import Vapor

struct Handler: APIProtocol {
    func getClip(_ input: Operations.getClip.Input) async throws -> Operations.getClip.Output {
        let clipResourceURL = Bundle.module.url(forResource: "cat", withExtension: "mp4")!
        let clipData = try Data(contentsOf: clipResourceURL)
        return .ok(Operations.getClip.Output.Ok(body: .binary(clipData)))
    }
    
    func getEmoji(_ input: Operations.getEmoji.Input) async throws -> Operations.getEmoji.Output {
        let candidates = "🐱😹😻🙀😿😽😸😺😾😼"
        let chosen = String(candidates.randomElement()!)
        let count = input.query.count ?? 1
        let emojis = String(repeating: chosen, count: count)
        return .ok(Operations.getEmoji.Output.Ok(body: .text(emojis)))
    }
}

@main
struct CatService {
    public static func main() throws {
        let app = Vapor.Application()
        let transport = VaporTransport(routesBuilder: app)
        let handler = Handler()
        try handler.registerHandlers(on: transport, serverURL: Servers.server1())
        try app.run()
    }
}

Resources