🍎 Swift

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. Это делает код чище и понятнее. Если ты