Irbisa · Шпаргалка

Сеть в iOS

URLSession, Codable, async-API, интерцепторы, обработка ошибок

Middle-разработчик · 25 вопросов

  1. 01

    Что такое REST и чем отличаются GET, POST, PUT, PATCH, DELETE и коды ответа HTTP?

    REST представляет собой соглашение для HTTP API: URL называет ресурс, HTTP-метод говорит, что с ним сделать, а код ответа сообщает клиенту, чем всё закончилось; его первая цифра задаёт класс.

  2. 02

    Что происходит, когда приложение делает HTTP-запрос, и как отправить его через URLSession?

    Система резолвит хост через DNS, открывает TCP- или QUIC-соединение, договаривается о TLS, отправляет метод, заголовки и тело, а затем читает статус, заголовки и тело ответа; в Swift всё это делает один вызов try await session.data(for:).

    // GETlet url = URL(string: "https://api.example.com/users")!let (data, response) = try await URLSession.shared.data(from: url)guard let http = response as? HTTPURLResponse, http.statusCode == 200 else {  throw URLError(.badServerResponse)}…
  3. 03

    Чем отличаются URL и URLRequest?

    URL содержит только адрес (схему, хост, путь и query), а URLRequest оборачивает URL всем, что нужно для отправки: HTTP-методом, заголовками, телом, таймаутом и политикой кеширования.

    var components = URLComponents(string: "https://api.example.com/users/42")!components.queryItems = [URLQueryItem(name: "fields", value: "name,avatar")]let url = components.url!                       // just the address var request = URLRequest(url: url)              // address + how to send itrequest.httpMethod = "PATCH"…
  4. 04

    Чем отличаются задачи data, download и upload в URLSession?

    Data task возвращает тело ответа в памяти, download task пишет его во временный файл и отдаёт URL этого файла, а upload task отправляет тело из памяти или прямо из файла.

  5. 05

    Что такое App Transport Security и как разрешить домен, который работает только по обычному HTTP?

    App Transport Security представляет собой системную политику, из-за которой URLSession и WKWebView по умолчанию отказываются от небезопасных соединений: обычного HTTP, TLS версии ниже 1.2, а также сертификатов и шифров слабее минимума Apple.

    <key>NSAppTransportSecurity</key><dict>  <key>NSExceptionDomains</key>  <dict>    <key>legacy-cdn.example.com</key>    <dict>…
  6. 06

    Этот поиск ничего не находит по "C++ & Swift", хотя в браузере тот же текст работает. Что не так со сборкой URL?

    func search(_ term: String, page: Int) async throws -> Data {  let url = URL(string: "https://api.example.com/search?q=\(term)&page=\(page)")!  let (data, _) = try await URLSession.shared.data(from: url)  return data} // search("C++ & Swift", page: 1)

    Поисковая строка вставлена в URL интерполяцией, поэтому её & начинает новый параметр, и сервер получает q=C++ ; значения query передают через URLComponents и URLQueryItem, которые кодируют каждое значение.

  7. 07

    Какие бывают URLSessionConfiguration (default, ephemeral и background) и когда какую использовать?

    default хранит кеш, cookie и учётные данные на диске; ephemeral держит их только в памяти и теряет вместе с сессией; background передаёт загрузки системному процессу, чтобы они продолжались, пока приложение приостановлено или завершено.

  8. 08

    Этот загрузчик вызывается для каждой картинки в ленте. Что с ним не так и чего это стоит?

    final class ImageLoader: NSObject, URLSessionDataDelegate {  func load(_ url: URL) async throws -> Data {    let session = URLSession(configuration: .default,                             delegate: self, delegateQueue: nil)    let (data, _) = try await session.data(from: url)    return data  }}

    Он создаёт новую URLSession на каждый запрос: каждый вызов заново платит за TCP- и TLS-handshake, потому что пул соединений принадлежит сессии, а каждая сессия держит свой делегат, пока её не инвалидируют, поэтому ничего не освобождается.

  9. 09

    Как бы вы построили сетевой слой поверх URLSession и какие ошибки он должен различать?

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

    protocol HTTPTransport: Sendable {  func data(for request: URLRequest) async throws -> (Data, URLResponse)}extension URLSession: HTTPTransport {  // explicit: a method with a defaulted `delegate:` does not satisfy the requirement  func data(for request: URLRequest) async throws -> (Data, URLResponse) {…
  10. 10

    Когда использовать URLSession напрямую, а когда брать библиотеку вроде Alamofire?

    Современный URLSession с async/await и Codable закрывает потребности большинства приложений в паре сотен строк вашего кода; Alamofire окупается, когда нужны готовые интерцепторы, политики ретраев, валидация, multipart и логирование, а не их написание и тестирование своими силами.

  11. 11

    Декодирование падает с «Expected date string to be ISO8601-formatted», хотя created_at выглядит обычной ISO-датой. Что не нравится JSONDecoder?

    .iso8601 строгая: ей нужна полная дата со временем и часовым поясом, например 2026-03-01T12:00:00Z, поэтому дата без времени, время без Z или смещения и пробел вместо T приводят к ошибке; старые версии ОС отвергали и дробные секунды.

    nonisolated(unsafe) private let fractional: ISO8601DateFormatter = {  let f = ISO8601DateFormatter()  f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]  return f}()nonisolated(unsafe) private let plain = ISO8601DateFormatter()   // no fractions…
  12. 12

    Бэкенд добавил новый тип поста, и вместо пропуска одной строки пустеет вся лента. Как сделать декодирование устойчивым?

    Декодирование массива работает по принципу «всё или ничего»: JSONDecoder бросает исключение на первом элементе, который не смог собрать, и вы теряете остальные сорок девять.

    enum PostKind: String, Decodable {  case text, photo, video, unknown   init(from decoder: Decoder) throws {    let raw = try decoder.singleValueContainer().decode(String.self)    self = PostKind(rawValue: raw) ?? .unknown     // new server value -> unknown…
  13. 13

    Как отменять сетевые запросы и обрабатывать timeout в Swift?

    Отменяйте Task, который ждёт запрос: он отменяет нижележащий URLSessionTask и бросает CancellationError или URLError(.cancelled); ожидание ограничивайте через timeoutIntervalForRequest и timeoutIntervalForResource, а отмену не показывайте пользователю как ошибку.

    // Cancel-previous typeahead with debounce@MainActorfinal class SearchModel {  private var task: Task<Void, Never>?  private let api: SearchAPI  var results: [Item] = []…
  14. 14

    Найдите баг: когда бэкенд отдаёт страницу ошибки, экран профиля бесконечно крутит спиннер вместо того, чтобы показать ошибку.

    func fetchProfile(id: Int) async throws -> Profile {    try await withCheckedThrowingContinuation { continuation in        LegacyAPI.shared.get("/profiles/\(id)") { data, error in            if let error {                continuation.resume(throwing: error)                return            }            guard let data,                  let profile = try? JSONDecoder().decode(Profile.self, from: data) else {                return            }            continuation.resume(returning: profile)        }    }}

    guard … else { return } выходит, не возобновив continuation, поэтому когда декодирование падает (HTML-страница 502, изменившийся контракт) или data равно nil, ожидающая задача остаётся приостановленной навсегда и спиннер не останавливается.

  15. 15

    Соединение пропадает посреди запроса. Как должен реагировать сетевой слой и почему не стоит проверять доступность сети перед каждым вызовом?

    Обрыв после начала запроса завершает его ошибкой URLError.networkConnectionLost: идемпотентный запрос повторяют с задержкой, а waitsForConnectivity включают, чтобы запросы, которые ещё не смогли стартовать, ждали сеть; проверять доступность сети перед вызовом не нужно.

    let config = URLSessionConfiguration.defaultconfig.waitsForConnectivity = true               // queue rather than failconfig.timeoutIntervalForResource = 120          // but not foreverconfig.allowsConstrainedNetworkAccess = false    // skip Low Data Modelet session = URLSession(configuration: config, delegate: MonitorDelegate(), delegateQueue: nil)…
  16. 16

    Какие упавшие запросы вы повторяете и как повторять, не устраивая лавину на сервер?

    Повторять стоит только те ошибки, которые следующая попытка в принципе может исправить.

    func withRetry<T>(  attempts: Int = 3,  base: Duration = .milliseconds(300),  isRetryable: (Error) -> Bool = APIError.isTransient,  operation: () async throws -> T) async throws -> T {…
  17. 17

    Как кешировать HTTP-ответы в iOS и за что отвечают URLCache, Cache-Control и ETag?

    URLCache работает как HTTP-кеш, встроенный в URLSession: Cache-Control: max-age позволяет ответить вообще без запроса, а ETag даёт перепроверить ответ, и тогда сервер отвечает 304 без тела.

    let config = URLSessionConfiguration.defaultconfig.urlCache = URLCache(memoryCapacity: 32 << 20, diskCapacity: 256 << 20)config.requestCachePolicy = .useProtocolCachePolicylet session = URLSession(configuration: config) // Explicit conditional request, when you keep the copy yourself…
  18. 18

    Как загрузить файл через multipart/form-data с URLSession?

    В URLSession нет сборщика multipart, поэтому тело вы собираете сами: уникальная строка-boundary, по одной части на поле со своим заголовком Content-Disposition и закрывающий boundary с двумя лишними дефисами на конце.

    func uploadAvatar(_ image: Data, name: String) async throws -> Data {  let boundary = "Boundary-\(UUID().uuidString)"  var body = Data()  func append(_ s: String) { body.append(s.data(using: .utf8)!) }   // Text field…
  19. 19

    Как тестировать код, который ходит через URLSession, не выходя в сеть?

    Хватает двух приёмов: подставлять фейк через протокол, когда вы тестируете собственный клиент, и регистрировать наследника URLProtocol, когда нужно прогнать настоящий путь загрузки.

    final class StubURLProtocol: URLProtocol {  nonisolated(unsafe) static var handler: ((URLRequest) throws -> (HTTPURLResponse, Data))?   override class func canInit(with request: URLRequest) -> Bool { true }  override class func canonicalRequest(for request: URLRequest) -> URLRequest { request }  override func stopLoading() {}…
  20. 20

    Когда выбирать WebSocket вместо HTTP-запросов и что нужно клиенту на URLSessionWebSocketTask?

    WebSocket держит одно двустороннее соединение открытым, чтобы сервер мог присылать сообщения сразу, как они появились (котировки, чат, мультиплеер); URLSessionWebSocketTask даёт отправку и приём, а цикл чтения, поддержание связи и переподключение вы пишете сами.

    final class PriceSocket {  private let session: URLSession  private var task: URLSessionWebSocketTask?   init(session: URLSession = .shared) { self.session = session }…
  21. 21

    Как работает аутентификация по токенам в приложении и как обновить access-токен, когда много запросов одновременно получили 401?

    Приложение отправляет с каждым запросом короткоживущий access-токен (часто JWT) и хранит долгоживущий refresh-токен, чтобы получать новый; когда несколько запросов одновременно получают 401, они должны разделить одно обновление через актор, а затем каждый повторяется один раз.

    actor TokenStore {  private(set) var accessToken: String?  private var refreshTask: Task<String, Error>?  private let refresh: @Sendable () async throws -> String   // calls /auth/refresh directly   init(accessToken: String?, refresh: @escaping @Sendable () async throws -> String) {…
  22. 22

    Эндпоинт экспорта отдаёт 400 МБ NDJSON, и на старых телефонах data(for:) приводит к убийству приложения по памяти. Что вызывать вместо него?

    data(for:) перед возвратом складывает всё тело в память, а bytes(for:) отдаёт ответ как AsyncSequence, который вы разбираете по мере поступления, поэтому пик памяти равен одной записи, а не всей выгрузке.

    struct Row: Decodable { let id: String; let amount: Decimal } func importRows(from url: URL, session: URLSession, store: RowStore) async throws {  let (bytes, response) = try await session.bytes(from: url)  guard let http = response as? HTTPURLResponse, http.statusCode == 200 else {    throw URLError(.badServerResponse)          // check before consuming…
  23. 23

    Что такое certificate pinning, как реализовать его в iOS и что может пойти не так?

    Пиннинг заставляет приложение принимать сервер, только если в цепочке его сертификата есть ключ, зашитый в приложение, поэтому man-in-the-middle с сертификатом от любого другого доверенного CA (корпоративного прокси, скомпрометированного CA, корневого сертификата, который пользователя уговорили установить) будет отвергнут.

    final class PinningDelegate: NSObject, URLSessionDelegate {  private let pins: Set<Data>          // SHA-256 of each pinned key: current + backup  init(pins: Set<Data>) { self.pins = pins }   func urlSession(_ session: URLSession,                  didReceive challenge: URLAuthenticationChallenge,…
  24. 24

    Видео на два гигабайта должно докачаться, даже если пользователь вышел из приложения. Как это сделать?

    Фоновая сессия передаёт загрузку системному демону, который продолжает работу, пока приложение выгружено или убито, и сообщает о результате, перезапустив приложение.

    final class Downloader: NSObject, URLSessionDownloadDelegate, @unchecked Sendable {  static let shared = Downloader()  var backgroundCompletion: (() -> Void)?       // touched on the main queue only   // Creating it again after a relaunch reconnects to the system's transfers  lazy var session: URLSession = {…
  25. 25

    Авторизация, логирование, ретраи и метрики должны оборачивать каждый запрос, но клиент не должен превратиться в god object. Как это скомпоновать?

    Опишите каждую сквозную задачу как middleware, который получает запрос и замыкание next, обязанное быть вызванным, а затем сверните массив в одну функцию отправки: позиция в массиве задаёт порядок вложенности, а порядок вложенности задаёт поведение.

    typealias Send = @Sendable (URLRequest) async throws -> (Data, HTTPURLResponse) protocol HTTPMiddleware: Sendable {  func intercept(_ request: URLRequest, next: Send) async throws -> (Data, HTTPURLResponse)}…