Trabajo con redes y API HTTP en Swift: guía detallada
Что это такое и зачем? (аналогия из реальной жизни)
Представь, что ты сидишь в уютном кафе и хочешь заказать пиццу. Ты не идёшь на кухню и не готовишь её сам — ты просто зовёшь официанта, говоришь ему свой заказ, а через некоторое время он приносит тебе готовое блюдо. В мире программирования всё устроено точно так же.
Работа с сетью и HTTP API — это способ для твоей программы (клиента) попросить что-то у другой программы (сервера), которая находится где-то далеко в интернете. Твоя программа — это ты в кафе, а сервер — это кухня ресторана. Официантом в этом случае выступает протокол HTTP (HyperText Transfer Protocol — протокол передачи гипертекста).
Зачем это нужно? Представь, что ты пишешь приложение для прогноза погоды. Ты же не будешь сам облетать весь мир с термометром? Вместо этого твоя программа просто отправляет запрос на сервер погоды, получает ответ и показывает пользователю температуру. Или, например, приложение для заказа такси: оно отправляет запрос, где находится машина, и получает ответ с координатами.
Важно: HTTP API (Application Programming Interface — интерфейс программирования приложений) — это набор правил, по которым программы общаются друг с другом через интернет. Это как меню в ресторане: ты можешь заказать только то, что написано в меню, и ровно так, как там написано.
В Swift мы используем специальные инструменты, чтобы наша программа могла «разговаривать» с серверами. Самый популярный из них — URLSession. Это как телефонная трубка, через которую мы звоним на сервер и передаём наш заказ.
Теория: как это работает изнутри
Давай заглянем «под капот» и разберёмся, что происходит, когда твоя программа отправляет запрос в интернет.
1. URL — адрес в интернете
Каждый ресурс в интернете имеет свой уникальный адрес — URL (Uniform Resource Locator — унифицированный указатель ресурса). Он состоит из нескольких частей:
| Часть URL | Пример | Что это значит |
|---|---|---|
| Схема (scheme) | https:// |
Как мы будем общаться (защищённое соединение) |
| Хост (host) | api.weather.com |
Имя сервера, к которому обращаемся |
| Порт (port) | :443 |
«Дверь», в которую стучимся (обычно не указывают) |
| Путь (path) | /v2/forecast |
Какой именно ресурс нам нужен |
| Параметры (query) | ?city=Moscow&days=3 |
Дополнительные данные для запроса |
2. HTTP-методы — что мы хотим сделать
Когда мы обращаемся к серверу, мы говорим ему не только «дай мне данные», но и уточняем, что именно мы хотим сделать. Для этого существуют HTTP-методы:
| Метод | Действие | Аналогия из жизни |
|---|---|---|
GET |
Получить данные | «Покажи меню» |
POST |
Создать новые данные | «Сделай новый заказ» |
PUT |
Обновить данные полностью | «Замени мой заказ на новый» |
PATCH |
Обновить данные частично | «Добавь сыр в мой заказ» |
DELETE |
Удалить данные | «Отмени мой заказ» |
3. Заголовки (headers) — дополнительная информация
Когда мы отправляем запрос, мы можем прикрепить к нему «записку» с дополнительной информацией. Например, мы можем сказать серверу: «Я ожидаю ответ в формате JSON» или «Вот мой ключ доступа, чтобы ты знал, что я свой».
4. Тело запроса (body) — данные, которые мы отправляем
Если мы хотим не просто получить данные, а отправить их (например, зарегистрировать нового пользователя), мы кладём эти данные в «тело» запроса. Обычно данные упаковываются в формат JSON (JavaScript Object Notation) — это как коробка с подписанными отсеками.
5. Ответ сервера
Сервер обрабатывает наш запрос и возвращает ответ. В ответе есть:
- Код состояния (status code) — число, которое говорит, всё ли прошло успешно. Например,
200— «Всё отлично!»,404— «Не найдено»,500— «Ошибка на сервере». - Заголовки ответа — служебная информация от сервера.
- Тело ответа — сами данные, которые мы просили (обычно в формате JSON).
6. Асинхронность — почему мы не ждём
Самое важное: работа с сетью — это асинхронный процесс. Это значит, что когда твоя программа отправляет запрос, она не замирает в ожидании ответа. Вместо этого она говорит: «Я отправляю запрос, а когда придёт ответ — разбуди меня». Это как если бы ты заказал пиццу и продолжил играть в телефон, пока её готовят. Когда пицца готова, тебя зовут.
В Swift для этого используются замыкания (closures) или async/await (современный способ, появившийся в Swift 5.5).
Синтаксис в Swift: все формы и варианты
В Swift есть несколько способов работать с сетью. Давай разберём их все, от самого простого до самого современного.
1. URLSession — главный инструмент
URLSession — это класс, который управляет всеми сетевыми запросами. Представь его как диспетчера, который распределяет заказы между курьерами.
// Создаём сессию (диспетчера)
let session = URLSession.shared
// .shared — это общая сессия, которую можно использовать для простых запросов
2. URLRequest — настройка запроса
Если нам нужно настроить запрос (добавить заголовки, изменить метод), мы используем URLRequest.
// Создаём URL
let url = URL(string: "https://api.example.com/users")!
// Создаём запрос на основе URL
var request = URLRequest(url: url)
// Настраиваем запрос
request.httpMethod = "POST" // Говорим, что хотим создать новые данные
request.setValue("application/json", forHTTPHeaderField: "Content-Type") // Говорим, что отправляем JSON
request.setValue("Bearer myToken123", forHTTPHeaderField: "Authorization") // Передаём ключ доступа
// Подготавливаем данные для отправки
let userData = ["name": "Иван", "age": 25]
let jsonData = try? JSONSerialization.data(withJSONObject: userData)
request.httpBody = jsonData // Кладём данные в тело запроса
3. Замыкания (closures) — старый способ
Раньше в Swift использовали замыкания для обработки ответа. Это как сказать: «Когда придёт ответ, сделай вот это».
let task = session.dataTask(with: url) { data, response, error in
// Этот код выполнится, когда придёт ответ от сервера
if let error = error {
print("Ошибка: (error.localizedDescription)")
return
}
if let data = data {
// data — это сырые данные (байты), которые прислал сервер
print("Получено (data.count) байт")
}
}
task.resume() // Обязательно! Запускаем задачу
4. async/await — современный способ
Начиная с Swift 5.5, появился новый способ работы с асинхронностью — async/await. Это делает код похожим на обычный последовательный код.
// Функция должна быть async, чтобы использовать await
func fetchUsers() async throws -> [User] {
let url = URL(string: "https://api.example.com/users")!
// await — говорим: "подожди здесь, пока придёт ответ"
let (data, response) = try await URLSession.shared.data(from: url)
// Проверяем код ответа
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw NetworkError.badResponse
}
// Декодируем JSON в массив пользователей
let users = try JSONDecoder().decode([User].self, from: data)
return users
}
5. Codable — работа с JSON
Чтобы удобно превращать JSON в объекты Swift и обратно, используется протокол Codable.
// Определяем структуру, которая соответствует JSON
struct User: Codable {
let id: Int
let name: String
let email: String
}
// Декодирование (JSON -> объект)
let jsonString = """
{
"id": 1,
"name": "Иван",
"email": "ivan@example.com"
}
"""
let jsonData = jsonString.data(using: .utf8)!
let user = try JSONDecoder().decode(User.self, from: jsonData)
// Кодирование (объект -> JSON)
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted // Делаем JSON красивым
let encodedData = try encoder.encode(user)
let jsonStringOutput = String(data: encodedData, encoding: .utf8)!
Примеры кода — от простого к сложному
Пример 1: Простой GET-запрос (получение данных)
import Foundation
// 1. Создаём URL — адрес, по которому будем стучаться
let urlString = "https://jsonplaceholder.typicode.com/posts/1"
guard let url = URL(string: urlString) else {
print("Ошибка: неверный URL")
exit(1)
}
// 2. Создаём задачу (task) — это как заказ в ресторане
let task = URLSession.shared.dataTask(with: url) { data, response, error in
// 3. Проверяем, не было ли ошибки при выполнении запроса
if let error = error {
print("Ошибка сети: (error.localizedDescription)")
return
}
// 4. Проверяем, что ответ существует и это HTTP-ответ
guard let httpResponse = response as? HTTPURLResponse else {
print("Ошибка: неверный тип ответа")
return
}
// 5. Проверяем код состояния (200 — успех)
print("Код ответа: (httpResponse.statusCode)")
// 6. Проверяем, что данные существуют
guard let data = data else {
print("Ошибка: нет данных в ответе")
return
}
// 7. Превращаем сырые данные в строку и выводим
if let jsonString = String(data: data, encoding: .utf8) {
print("Полученные данные:")
print(jsonString)
}
}
// 8. Запускаем задачу (без этого ничего не произойдёт!)
task.resume()
// 9. Ждём, чтобы программа не завершилась сразу
// (в реальном приложении так делать не нужно)
sleep(2)
Пример 2: POST-запрос (отправка данных)
import Foundation
// 1. Создаём URL
let url = URL(string: "https://jsonplaceholder.typicode.com/posts")!
// 2. Создаём запрос и настраиваем его
var request = URLRequest(url: url)
request.httpMethod = "POST" // Говорим, что хотим создать новый пост
request.setValue("application/json", forHTTPHeaderField: "Content-Type") // Отправляем JSON
// 3. Подготавливаем данные для отправки
let newPost: [String: Any] = [
"title": "Мой первый пост",
"body": "Это текст моего первого поста на Swift",
"userId": 1
]
// 4. Превращаем словарь в JSON-данные
do {
let jsonData = try JSONSerialization.data(withJSONObject: newPost)
request.httpBody = jsonData
} catch {
print("Ошибка при создании JSON: (error)")
exit(1)
}
// 5. Отправляем запрос
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
print("Ошибка: (error)")
return
}
guard let httpResponse = response as? HTTPURLResponse else {
print("Неверный ответ")
return
}
print("Статус: (httpResponse.statusCode)") // Должно быть 201 (Created)
if let data = data, let responseString = String(data: data, encoding: .utf8) {
print("Ответ сервера:")
print(responseString)
}
}
task.resume()
sleep(2)
Пример 3: Работа с Codable (структурированные данные)
import Foundation
// Определяем структуры, которые соответствуют JSON
struct Post: Codable {
let userId: Int
let id: Int
let title: String
let body: String
}
struct NewPost: Codable {
let title: String
let body: String
let userId: Int
}
// Функция для получения поста
func fetchPost(id: Int) {
let url = URL(string: "https://jsonplaceholder.typicode.com/posts/(id)")!
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data, error == nil else {
print("Ошибка: (error?.localizedDescription ?? "неизвестная ошибка")")
return
}
do {
// Декодируем JSON в структуру Post
let post = try JSONDecoder().decode(Post.self, from: data)
print("Пост #(post.id):")
print("Заголовок: (post.title)")
print("Текст: (post.body)")
print("Автор: (post.userId)")
} catch {
print("Ошибка декодирования: (error)")
}
}
task.resume()
}
// Вызываем функцию
fetchPost(id: 1)
sleep(2)
Пример 4: async/await (современный способ)
import Foundation
// Определяем возможные ошибки сети
enum NetworkError: Error {
case invalidURL
case badResponse
case decodingError
}
// Структура для пользователя
struct User: Codable {
let id: Int
let name: String
let username: String
let email: String
}
// Асинхронная функция для получения пользователей
func fetchUsers() async throws -> [User] {
// 1. Создаём URL
guard let url = URL(string: "https://jsonplaceholder.typicode.com/users") else {
throw NetworkError.invalidURL
}
// 2. Отправляем запрос и ждём ответ (await)
let (data, response) = try await URLSession.shared.data(from: url)
// 3. Проверяем код ответа
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw NetworkError.badResponse
}
// 4. Декодируем данные
do {
let users = try JSONDecoder().decode([User].self, from: data)
return users
} catch {
throw NetworkError.decodingError
}
}
// Функция для запуска асинхронной операции
func main() async {
do {
let users = try await fetchUsers()
print("Получено (users.count) пользователей:")
for user in users {
print("- (user.name) ((user.email))")
}
} catch {
print("Ошибка: (error)")
}
}
// Запускаем асинхронную функцию
Task {
await main()
}
sleep(2)
Пример 5: Загрузка изображения
import Foundation
// Функция для загрузки изображения
func downloadImage(from urlString: String) {
// 1. Проверяем URL
guard let url = URL(string: urlString) else {
print("Неверный URL")
return
}
// 2. Создаём задачу для загрузки данных
let task = URLSession.shared.dataTask(with: url) { data, response, error in
// 3. Проверяем на ошибки
if let error = error {
print("Ошибка загрузки: (error.localizedDescription)")
return
}
// 4. Проверяем, что данные — это изображение
guard let data = data,
let image = UIImage(data: data) else {
print("Не удалось создать изображение из данных")
return
}
// 5. Сохраняем изображение (в реальном приложении показываем на экране)
print("Изображение загружено! Размер: (image.size)")
print("Размер данных: (data.count) байт")
// Можно сохранить в файл
let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let fileURL = documentsPath.appendingPathComponent("downloaded_image.jpg")
try? data.write(to: fileURL)
print("Изображение сохранено в: (fileURL.path)")
}
task.resume()
}
// Загружаем картинку
downloadImage(from: "https://picsum.photos/200/300")
sleep(3)
Пример 6: Обработка ошибок и повторные попытки
import Foundation
// Функция для выполнения запроса с повторными попытками
func fetchWithRetry(url: URL, maxRetries: Int = 3) async throws -> Data {
var lastError: Error?
for attempt in 1...maxRetries {
do {
print("Попытка (attempt) из (maxRetries)...")
let (data, response) = try await URLSession.shared.data(from: url)
// Проверяем код ответа
guard let httpResponse = response as? HTTPURLResponse else {
throw NetworkError.badResponse
}
// Если 429 (Too Many Requests) — ждём и пробуем снова
if httpResponse.statusCode == 429 {
print("Слишком много запросов. Ждём...")
try await Task.sleep(nanoseconds: 2_000_000_000) // 2 секунды
continue
}
// Если успех — возвращаем данные
guard httpResponse.statusCode == 200 else {
throw NetworkError.badResponse
}
return data
} catch {
lastError = error
print("Попытка (attempt) не удалась: (error.localizedDescription)")
// Если это не последняя попытка — ждём перед повторением
if attempt < maxRetries {
let delay = UInt64(attempt * 1_000_000_000) // Увеличиваем задержку
try await Task.sleep(nanoseconds: delay)
}
}
}
throw lastError ?? NetworkError.badResponse
}
// Использование
func main() async {
let url = URL(string: "https://jsonplaceholder.typicode.com/posts/1")!
do {
let data = try await fetchWithRetry(url: url)
if let jsonString = String(data: data, encoding: .utf8) {
print("Успешно получено:")
print(jsonString.prefix(200))
}
} catch {
print("Все попытки исчерпаны: (error)")
}
}
Task {
await main()
}
sleep(5)
Пример 7: Полноценный REST-клиент
import Foundation
// Протокол для API-клиента
protocol APIClient {
func get(_ path: String) async throws -> T
func post(_ path: String, body: U) async throws -> T
func delete(_ path: String) async throws
}
// Реализация клиента
class RESTClient: APIClient {
private let baseURL: String
private let session: URLSession
private let decoder: JSONDecoder
private let encoder: JSONEncoder
init(baseURL: String) {
self.baseURL = baseURL
self.session = URLSession.shared
self.decoder = JSONDecoder()
self.encoder = JSONEncoder()
}
// GET-запрос
func get(_ path: String) async throws -> T {
guard let url = URL(string: "(baseURL)(path)") else {
throw NetworkError.invalidURL
}
let (data, response) = try await session.data(from: url)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw NetworkError.badResponse
}
return try decoder.decode(T.self, from: data)
}
// POST-запрос
func post(_ path: String, body: U) async throws -> T {
guard let url = URL(string: "(baseURL)(path)") else {
throw NetworkError.invalidURL
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try encoder.encode(body)
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 201 else {
throw NetworkError.badResponse
}
return try decoder.decode(T.self, from: data)
}
// DELETE-запрос
func delete(_ path: String) async throws {
guard let url = URL(string: "(baseURL)(path)") else {
throw NetworkError.invalidURL
}
var request = URLRequest(url: url)
request.httpMethod = "DELETE"
let (_, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 || httpResponse.statusCode == 204 else {
throw NetworkError.badResponse
}
}
}
// Использование клиента
struct Todo: Codable {
let id: Int
let title: String
let completed: Bool
}
struct NewTodo: Codable {
let title: String
let completed: Bool
let userId: Int
}
func main() async {
let client = RESTClient(baseURL: "https://jsonplaceholder.typicode.com")
// Получаем список задач
do {
let todos: [Todo] = try await client.get("/todos")
print("Получено (todos.count) задач")
print("Первая задача: (todos[0].title)")
} catch {
print("Ошибка получения задач: (error)")
}
// Создаём новую задачу
do {
let newTodo = NewTodo(title: "Выучить Swift", completed: false, userId: 1)
let created: Todo = try await client.post("/todos", body: newTodo)
print("Создана задача: (created.title) (id: (created.id))")
} catch {
print("Ошибка создания задачи: (error)")
}
}
Task {
await main()
}
sleep(3)
Сравнение с Python: чем похоже и чем отличается
| Аспект | Swift | Python |
|---|---|---|
| Основная библиотека | URLSession (встроенная) |
requests (сторонняя, нужно устанавливать) |
| Простота GET-запроса | Нужно создать задачу, запустить её, обработать в замыкании | response = requests.get(url) — одна строка |
| Асинхронность | Встроенная, обязательная (сеть всегда асинхронна) | По умолчанию синхронная, но есть asyncio |
| Работа с JSON | Codable — декодирование в структуры |
response.json() — сразу в словарь |
| Типобезопасность | Строгая: нужно точно указать типы данных | Динамическая: можно работать со словарями |
| Обработка ошибок | do-catch с собственными типами ошибок |
try-except с исключениями |
| Производительность | Высокая (нативный код) | Средняя (интерпретируемый язык) |
Важное отличие: В Python ты можешь написать requests.get(url).json() и сразу получить данные. В Swift так не получится — нужно обязательно работать асинхронно. Это связано с тем, что iOS-приложения не должны «зависать» в ожидании ответа от сервера.
Типичные ошибки и подводные камни
1. Забыл вызвать task.resume()
Самая распространённая ошибка новичков. Ты создал задачу, но забыл её запустить. Запрос не уйдёт, и ты будешь ждать ответа вечно.
// ❌ Неправильно
let task = URLSession.shared.dataTask(with: url) { ... }
// Задача создана, но не запущена!
// ✅ Правильно
let task = URLSession.shared.dataTask(with: url) { ... }
task.resume() // Обязательно запускаем!
2. Работа с сетью на главном потоке
Сетевые запросы должны выполняться в фоне, чтобы не блокировать интерфейс пользователя. URLSession делает это автоматически, но обновлять UI нужно на главном потоке.
// ❌ Неправильно: обновляем UI в фоновом потоке
URLSession.shared.dataTask(with: url) { data, response, error in
self.label.text = "Данные загружены" // Это вызовет ошибку!
}
// ✅ Правильно: переключаемся на главный поток
URLSession.shared.dataTask(with: url) { data, response, error in
DispatchQueue.main.async {
self.label.text = "Данные загружены"
}
}
3. Force unwrap (принудительное извлечение) URL
Использование ! для URL может привести к крашу приложения, если строка содержит недопустимые символы.
// ❌ Опасно
let url = URL(string: "https://api.example.com/путь с пробелами")!
// ✅ Безопасно
guard let url = URL(string: "https://api.example.com/путь%20с%20пробелами") else {
print("Неверный URL")
return
}
4. Игнорирование кодов состояния HTTP
Не все ответы с кодом 200 — успешны. Всегда проверяй код состояния.
// ❌ Неправильно: не проверяем код
let (data, _) = try await session.data(from: url)
// ✅ Правильно: проверяем код
let (data, response) = try await session.data(from: url)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw NetworkError.badResponse
}
5. Утечка памяти в замыканиях
Если в замыкании ты используешь self, может возникнуть циклическая ссылка.
// ❌ Опасно: может быть утечка памяти
URLSession.shared.dataTask(with: url) { data, response, error in
self.processData(data) // self захвачен сильно
}
// ✅ Правильно: используем weak self
URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
guard let self = self else { return }
self.processData(data)
}
6. Неправильное кодирование URL
Если в URL есть специальные символы (пробелы, кириллица), их нужно кодировать.
// ❌ Неправильно
let query = "Москва погода"
let url = URL(string: "https://api.weather.com/?city=(query)")!
// ✅ Правильно
let query = "Москва погода"
let encodedQuery = query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
let url = URL(string: "https://api.weather.com/?city=(encodedQuery)")!
Лучшие практики и советы профессионалов
1. Используй async/await вместо замыканий
Современный Swift (5.5+) поддерживает async/await. Это делает код чище и понятнее. Если ты