Урок по треку Swift и iOS 8 мин

Сеть, Codable и хранение данных в iOS

URLSession и async/await, Codable, UserDefaults, Keychain, Core Data и SwiftData, файлы.

Сетевой слой и хранение данных обсуждают на любом собесе: от «как декодировать JSON» до «как спроектировать обновление токена без гонок».

URLSession с async/await

struct APIClient {
    let session: URLSession
    let decoder: JSONDecoder

    func send<T: Decodable>(_ request: URLRequest, as type: T.Type) async throws -> T {
        let (data, response) = try await session.data(for: request)
        guard let http = response as? HTTPURLResponse else { throw APIError.invalidResponse }
        guard (200...299).contains(http.statusCode) else {
            throw APIError.status(http.statusCode, data)
        }
        return try decoder.decode(T.self, from: data)
    }
}

Главное: URLSession бросает только ошибки транспорта (нет сети, таймаут, отмена, TLS). Ответ 404 или 500 для неё успех, статус проверяете вы.

App Transport Security по умолчанию разрешает только HTTPS с TLS 1.2+, исключения настраиваются в Info.plist.

Фоновые передачи. URLSessionConfiguration.background(withIdentifier:) передаёт загрузку системе: она продолжится, даже если приложение приостановлено или выгружено. Upload в фоне только из файла.

HTTP-кеш. URLCache кеширует ответы по Cache-Control, ETag, Last-Modified. Для офлайн-режима API надёжнее своя база.

Codable

struct User: Codable {
    let id: Int
    let firstName: String
    let avatarURL: URL?        // поле может отсутствовать
    let createdAt: Date
}

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .iso8601

Правила:

  • отсутствующий ключ для неопционального свойства это DecodingError.keyNotFound для всей модели;
  • точечные расхождения имён решают CodingKeys;
  • нестандартные случаи (число то строкой, то числом) решает свой init(from:);
  • массив декодируется целиком: один битый элемент ломает всю ленту. Устойчивость даёт обёртка, которая ловит ошибку элемента и пропускает его с логированием.
struct Lossy<T: Decodable>: Decodable {
    let value: T?
    init(from decoder: Decoder) throws {
        value = try? T(from: decoder)
    }
}
let posts = try decoder.decode([Lossy<Post>].self, from: data).compactMap(\.value)

Обновление токена

Если несколько запросов одновременно получили 401, обновлять токен нужно один раз: refresh-токены часто одноразовые, и параллельный обмен выкинет пользователя на логин. Решение: актор хранит текущую задачу обновления, и все ждут её.

actor TokenRefresher {
    private var task: Task<String, Error>?
    func freshToken() async throws -> String {
        if let task { return try await task.value }
        let t = Task { try await api.refresh() }
        task = t
        defer { task = nil }
        return try await t.value
    }
}

Ретраи делают только для идемпотентных запросов, с экспоненциальной задержкой и случайным разбросом.

Где хранить данные

ДанныеХранилище
Токены, пароли, ключиKeychain
Мелкие настройки и флагиUserDefaults
Связанные данные с запросамиCore Data, SwiftData, SQLite
Документы пользователяфайлы в Documents (попадают в бэкап)
Восстановимый кешфайлы в Caches (может очищаться системой)
Служебные данные приложенияApplication Support

UserDefaults хранит plist открытым текстом и читается целиком, поэтому туда нельзя секреты и большие данные. UserDefaults входит в список required reason API: причину его использования нужно указать в privacy manifest.

Keychain. Доступность элемента задаётся атрибутом: WhenUnlocked (только при разблокированном устройстве) или AfterFirstUnlock (нужно для фоновых задач). Суффикс ThisDeviceOnly запрещает перенос на другое устройство через бэкап.

Core Data

  • NSPersistentContainer поднимает стек: модель, координатор, хранилище.
  • viewContext работает на главной очереди, для импорта используют фоновый контекст: container.performBackgroundTask { ctx in ... }.
  • Контекст и его объекты привязаны к своей очереди. Между контекстами передают objectID, а не объекты.
  • automaticallyMergesChangesFromParent = true у viewContext, чтобы UI видел фоновые сохранения.
  • Изменения схемы требуют миграции: лёгкой (автоматической) для простых случаев или ручной.

SwiftData (iOS 17)

@Model final class Note {
    var title: String
    var createdAt: Date
    init(title: String) { self.title = title; createdAt = .now }
}

struct NotesView: View {
    @Query(sort: \Note.createdAt, order: .reverse) var notes: [Note]
    @Environment(\.modelContext) private var context
}

Работает поверх движка Core Data, удобна в SwiftUI, но требует iOS 17 и пока менее зрелая на сложных схемах. Альтернатива для тех, кто хочет обычный SQL: SQLite через библиотеку вроде GRDB.

Частые ошибки

  • Не проверять статус ответа.
  • try? при декодировании и потерянная причина ошибки.
  • Токен в UserDefaults.
  • Использовать viewContext из фоновой очереди.
  • Кеш картинок в Documents.

Что спросят на собесе

  • Как устроен ваш сетевой слой, где обработка ошибок и ретраи?
  • Как декодировать snake_case, даты, отсутствующие поля?
  • Где хранить токен и почему не в UserDefaults?
  • Core Data и потоки: как импортировать много данных?
  • Как избежать параллельных обновлений токена?

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.