Integrating ChatGPT in Your iOS Apps

Sep 19 2024 · Swift 6, iOS 18, Xcode 16

Lesson 01: Introducing ChatGPT

Demo

Episode complete

Play next episode

Next
Transcript

In this demo, you’ll create a GPTClient to interact with ChatGPT.

You’ll need to have already registered for OpenAI, purchased tokens, and generated a project API key. If you haven’t done this already, go to platform.openai and complete this first.

Already registered? Then you’re ready to write your GPTClient.

Open Xcode, select File -> New -> Playground -> iOS -> Blank, and then click Next.

Name your new Playground GPTClient.

Under Sources, create two new files. GPTModels.swift will hold all of the model files. GPTClient.swift will hold the GPTClient type.

First, you’ll create types for GPTModelVersion and GPTMessage within GPTModels.swift:

public enum GPTModelVersion: String, Codable {
  case gpt35Turbo = "gpt-3.5-turbo"
  case gpt4Turbo = "gpt-4-turbo"
  case gpt4o = "gpt-4o"
}

public struct GPTMessage: Codable, Hashable {
  public let role: Role
  public let content: String

  public init(role: Role, content: String) {
    self.role = role
    self.content = content
  }

  public enum Role: String, Codable {
    case assistant
    case system
    case user
  }
}

GPTModelVersion is an enum that represents the GPT model, and GPTMessage represents the message within the body of a GPT chat request. The role and content are exactly as explained in the instruction section. You declare everything as Codable to make it easier to encode and decode as JSON later.

Note: Because you added this code to a Playground, you must explicitly declare the types as public to make them accessible. Likewise, you must declare a custom public initializer for structs, as the member-wise init is internal by default.

Declaring the publicly facing types, functions and properties as public is good practice. This is because if you move the GPTClient to another Swift module, you won’t have to make any changes for it to work.

To make it easier to create context messages, you can add an extension on Array:

public extension Array where Element == GPTMessage {
  static func makeContext(_ contents: String...) -> [GPTMessage] {
    return contents.map { GPTMessage(role: .system, content: $0)}
  }
}

This will allow you to easily create an array of GPTMessage objects from an array of String objects. You set the role for each as .system to indicate that these are meant to be used for context.

Next, we’ll create two more models for GPTChatRequest and GPTChatResponse.

struct GPTChatRequest: Codable {
  let model: GPTModelVersion
  let messages: [GPTMessage]

  init(model: GPTModelVersion,
              messages: [GPTMessage]) {
    self.model = model
    self.messages = messages
  }
}

public struct GPTChatResponse: Codable {
  public let choices: [Choice]

  let id: String
  let created: Date
  let model: String

  init(id: String, created: Date, model: String, choices: [Choice]) {
    self.id = id
    self.created = created
    self.model = model
    self.choices = choices
  }

  public struct Choice: Codable {
    public let message: GPTMessage
  }
}

You’ll use GPTChatRequest to create a request to the GPT chat completions endpoint and GPTChatResponse to get the response.

You also need to create two types to handle error cases:

enum GPTClientError: Error, CustomStringConvertible {
  case errorResponse(statusCode: Int, error: GPTErrorResponse?)
  case networkError(message: String? = nil, error: Error? = nil)

  var description: String {
    switch self {
    case .errorResponse(let statusCode, let error):
      return "GPTClientError.errorResponse: statusCode: \(statusCode), " +
      "error: \(String(describing: error))"

    case .networkError(let message, let error):
      return "GPTClientError.networkError: message:
        \(String(describing: message)), " +"error: \(String(describing:
        error))"
    }
  }
}

struct GPTErrorResponse: Codable {
  let error: ErrorDetail

  struct ErrorDetail: Codable {
    let message: String
    let type: String
    let param: String?
    let code: String?
  }
}

GPTClientError is simply a custom Error that you’ll throw if there’s either an HTTP error code or a network error.

You haven’t seen GPTErrorResponse yet, but this is pretty easy to understand. This is how ChatGPT will respond if there’s a problem with the request. For example, if you forget to include an OpenAI API Key, you won’t get a networking error, but you will get an error response in this format instead.

Next, you’ll define the GPTClient within GPTClient.swift:

public class GPTClient {

  var model: GPTModelVersion
  var context: [GPTMessage]

  let apiKey: String
  let encoder: JSONEncoder
  let decoder: JSONDecoder
  let urlSession: URLSession

  public init(apiKey: String,
       model: GPTModelVersion,
       context: [GPTMessage] = [],
       urlSession: URLSession = .shared) {
    self.apiKey = apiKey
    self.model = model
    self.context = context
    self.urlSession = urlSession

    let decoder = JSONDecoder()
    decoder.dateDecodingStrategy = .secondsSince1970
    self.decoder = decoder

    self.encoder = JSONEncoder()
  }
}

You first define properties for model and context, which you’ll use to create GPTChatRequest later. You define both as var instead of let properties to make them mutable.

You also declare several let properties for apiKey, encoder, decoder, and urlSession. These are properties that won’t ever change after a GPTClient is created.

Next, you need a method to help you create a request generally:

private func requestFor(url: URL, httpMethod: String, httpBody: Data?)
  -> URLRequest {

  var request = URLRequest(url: url)
  request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
  request.setValue("application/json", forHTTPHeaderField: "Content-Type")
  request.cachePolicy = .reloadIgnoringLocalCacheData
  request.httpMethod = "POST"
  request.httpBody = httpBody
  return request
}

This method makes it easy to create a URLRequest for any ChatGPT endpoint in general.

You can next use this method to actually create a request to the chat completions endpoint:

public func sendChats(_ chats: [GPTMessage]) async throws ->
  GPTChatResponse {

  do {
    let chatRequest = GPTChatRequest(model: model, messages: context
      + chats)
    return try await sendChatRequest(chatRequest)

  } catch let error as GPTClientError {
    throw error
  } catch {
    throw GPTClientError.networkError(error: error)
  }
}

private func sendChatRequest(_ chatRequest: GPTChatRequest) async
  throws -> GPTChatResponse {

  let data = try encoder.encode(chatRequest)

  let url = URL(string: "https://api.openai.com/v1/chat/completions")!
  let request = requestFor(url: url, httpMethod: "POST", httpBody: data)

  let (responseData, urlResponse) = try await urlSession.data(for: request)
  guard let httpResponse = urlResponse as? HTTPURLResponse else {
    throw GPTClientError.networkError(message:
      "URLResponse is not an HTTPURLResponse")
  }
  guard httpResponse.statusCode == 200 else {
    let errorResponse = try? decoder.decode(GPTErrorResponse.self,
      from: responseData)
    throw GPTClientError.errorResponse(statusCode: httpResponse.statusCode,
      error: errorResponse)
  }

  let chatResponse = try decoder.decode(GPTChatResponse.self,
    from: responseData)
  return chatResponse
}

Here’s how that works:

  • You’ll use sendChats to send messages to ChatGPT asynchronously.
  • sendChats converts the passed-in chats to a GPTChatRequest using both the model and context.
  • It then calls sendChatRequest, which handles encoding the GPTChatRequest, sending it using the urlSession, and then handles decoding either a GPTClientError in the case of failure or a GPTChatResponse if it’s successful.

Great! This takes care of the client. Now you’re ready to try it out.

Let’s create an instance of GPTClient on the main page for the Playground so you can run and try it out:

let client = GPTClient(apiKey: "{Paste your OpenAI API Key here}",
                       model: .gpt35Turbo,
                       context: .makeContext("Act as a scientist
                         but be brief"))

Remember that you MUST use your own OpenAI API key here. The one shown is temporary and won’t actually work.

Now try to send a chat!

let prompt = GPTMessage(role: .user, content: "How do humming birds fly?")

Task {
  do {
    let response = try await client.sendChats([prompt])
    print(response.choices.first?.message.content ?? "No choices received!")
  } catch {
    print("Got an error: \(error)")
  }
}

If everything went well, you should see a response printed to the console like this:

Hummingbirds fly by flapping their wings in a figure-eight pattern, allowing them to hover, fly backward, and maneuver with precision. Their rapid wing movement produces lift and thrust, enabling them to remain airborne and access nectar from flowers.

See forum comments
Cinema mode Download course materials from Github
Previous: Instruction 02 Next: Conclusion