Сетевой слой и хранение данных обсуждают на любом собесе: от «как декодировать 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 и потоки: как импортировать много данных?
- Как избежать параллельных обновлений токена?