Почему Swift и ChatGPT — это любовь с первого коммита
Представьте: вы пишете приложение на Swift, и вдруг понимаете, что ему не хватает «мозгов». Пользователь задаёт вопрос, а ваш код молчит, как рыба об лёд. Знакомо? До недавнего времени добавить интеллект в iOS-приложение было сложно: нужно было либо писать свою NLP-модель, либо подключать тяжёлые библиотеки. Теперь всё иначе.
ChatGPT API на Swift — это не просто модная фишка, а реальный инструмент, который может превратить ваше приложение в умного помощника. Хотите сделать AI-ассистента, генератор контента или просто чат-бота? Всё это можно реализовать за один вечер. В этой статье я покажу, как подключить OpenAI API в Swift с нуля, без лишней магии и сложных архитектур. Поехали!
Что нам понадобится?
Прежде чем прыгать в код, давайте разберёмся с инструментами. Для работы с ChatGPT в iOS вам нужно всего три вещи:
- API-ключ OpenAI — регистрируетесь на platform.openai.com, создаёте ключ в разделе API Keys. Храните его как зеницу ока — не коммитьте в Git!
- Xcode 15+ — чтобы использовать современный Swift с async/await.
- Базовое понимание SwiftUI или UIKit — я покажу пример на SwiftUI, но логика работы с сетью универсальна.
Готовы? Тогда открывайте Xcode, создавайте новый проект (iOS App, SwiftUI, язык — Swift) и поехали писать код.
Архитектура запроса: как «говорить» с ChatGPT на Swift
OpenAI предоставляет REST API. Это значит, что мы будем отправлять HTTP-запросы и получать JSON-ответы. Всё общение идёт через endpoint https://api.openai.com/v1/chat/completions. Нам нужно отправить POST-запрос с телом, содержащим массив сообщений (роль + контент), и получить ответ от модели.
Давайте создадим модель данных для запроса и ответа. Это идеальный пример использования Decodable в Swift.
// Модель для одного сообщения
struct Message: Codable {
let role: String // "system", "user" или "assistant"
let content: String
}
// Структура запроса к API
struct ChatGPTRequest: Codable {
let model: String
let messages: [Message]
let temperature: Double?
init(model: String = "gpt-4o-mini", messages: [Message], temperature: Double = 0.7) {
self.model = model
self.messages = messages
self.temperature = temperature
}
}
// Структура ответа
struct ChatGPTResponse: Codable {
struct Choice: Codable {
let message: Message
let finishReason: String?
enum CodingKeys: String, CodingKey {
case message
case finishReason = "finish_reason"
}
}
let choices: [Choice]
}
Обратите внимание на CodingKeys — в Swift имена свойств принято писать в camelCase, а API возвращает snake_case. Это стандартный приём для работы с API в Swift.
Сетевой слой: URLSession + async/await
Теперь самое интересное — отправка запроса. Я буду использовать современный async/await, чтобы код был чистым и читаемым. Никаких замыканий и делегатов — только красота.
class ChatGPTService {
private let apiKey: String
private let session = URLSession.shared
private let baseURL = URL(string: "https://api.openai.com/v1/chat/completions")!
init(apiKey: String) {
self.apiKey = apiKey
}
func sendMessage(_ text: String) async throws -> String {
// Формируем сообщения: system prompt + запрос пользователя
let systemMessage = Message(role: "system",
content: "Ты — полезный ассистент. Отвечай кратко и по делу.")
let userMessage = Message(role: "user", content: text)
let requestBody = ChatGPTRequest(messages: [systemMessage, userMessage])
// Настраиваем URLRequest
var urlRequest = URLRequest(url: baseURL)
urlRequest.httpMethod = "POST"
urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
urlRequest.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
urlRequest.httpBody = try JSONEncoder().encode(requestBody)
// Отправляем запрос
let (data, response) = try await session.data(for: urlRequest)
// Проверяем статус код
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
throw URLError(.badServerResponse)
}
// Декодируем ответ
let chatResponse = try JSONDecoder().decode(ChatGPTResponse.self, from: data)
// Возвращаем текст ответа
return chatResponse.choices.first?.message.content ?? "Нет ответа"
}
}
Всё гениальное просто. Мы создали класс ChatGPTService, который принимает API-ключ и умеет отправлять сообщения. Обратите внимание на system prompt — это ваша возможность задать модели стиль поведения. Хотите, чтобы ChatGPT отвечал как пират? Просто поменяйте текст промпта.
Подключаем к UI: SwiftUI View
Теперь давайте сделаем красивое окно чата. Минимум кода — максимум эффекта. Мы создадим ChatViewModel, который будет управлять состоянием, и простую SwiftUI View.
import SwiftUI
@MainActor
class ChatViewModel: ObservableObject {
@Published var messages: [Message] = []
@Published var isLoading = false
private let service: ChatGPTService
init(apiKey: String) {
self.service = ChatGPTService(apiKey: apiKey)
// Добавляем приветственное сообщение от ассистента
messages.append(Message(role: "assistant", content: "Привет! Чем могу помочь?"))
}
func send(_ text: String) {
guard !text.trimmingCharacters(in: .whitespaces).isEmpty else { return }
// Добавляем сообщение пользователя
let userMessage = Message(role: "user", content: text)
messages.append(userMessage)
isLoading = true
Task {
do {
let reply = try await service.sendMessage(text)
let assistantMessage = Message(role: "assistant", content: reply)
messages.append(assistantMessage)
} catch {
let errorMessage = Message(role: "assistant", content: "Ошибка: \(error.localizedDescription)")
messages.append(errorMessage)
}
isLoading = false
}
}
}
struct ChatView: View {
@StateObject private var viewModel = ChatViewModel(apiKey: "ВАШ_КЛЮЧ_ЗДЕСЬ")
@State private var inputText = ""
var body: some View {
VStack {
// Список сообщений
ScrollViewReader { proxy in
ScrollView {
LazyVStack(spacing: 12) {
ForEach(Array(viewModel.messages.enumerated()), id: \.offset) { index, message in
MessageBubble(message: message)
.id(index)
}
if viewModel.isLoading {
ProgressView()
.padding()
}
}
.padding()
}
.onChange(of: viewModel.messages.count) { _ in
withAnimation {
proxy.scrollTo(viewModel.messages.count - 1, anchor: .bottom)
}
}
}
// Поле ввода
HStack {
TextField("Введите сообщение...", text: $inputText)
.textFieldStyle