Настройка URLSession для iOS: аудит, оптимизация, избегаем утечек

TRUETECH занимается разработкой, поддержкой и обслуживанием мобильных приложений iOS, Android, PWA. Имеем большой опыт и экспертизу для публикации мобильных приложений в популярные маркеты Google Play, App Store, Amazon, AppGallery и другие.

Разработка и поддержка любых видов мобильных приложений:

Информационные и развлекательные мобильные приложения
Новостные приложения, игры, справочники, онлайн-каталоги, погодные, фитнес и здоровье, туристические, образовательные, социальные сети и мессенджеры, квиз, блоги и подкасты, форумы, агрегаторы
Мобильные приложения электронной коммерции
Интернет-магазины, B2B-приложения, маркетплейсы, онлайн-обменники, кэшбэк-сервисы, биржи, дропшиппинг-платформы, программы лояльности, доставка еды и товаров, платежные системы
Мобильные приложения для управления бизнес-процессами
CRM-системы, ERP-системы, управление проектами, инструменты для команды продаж, учет финансов, управление производством, логистика и доставка, управление персоналом, системы мониторинга данных
Мобильные приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, платформы предоставления электронных услуг, платформы кешбека, видеохостинги, тематические порталы, платформы онлайн-бронирования и записи, платформы онлайн-торговли

Это лишь некоторые из типы мобильных приложений, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента.

Услуги, которые мы предлагаем
Показано 1 из 1Все 1734 услуг
Настройка URLSession для iOS: аудит, оптимизация, избегаем утечек
Средний
от 1 дня до 3 дней
Часто задаваемые вопросы

Наши компетенции:

Этапы разработки

Последние работы

  • image_mobile-applications_feedme_467_0.webp
    Разработка мобильного приложения для компании FEEDME
    858
  • image_mobile-applications_xoomer_471_0.webp
    Разработка мобильного приложения для компании XOOMER
    743
  • image_mobile-applications_rhl_428_0.webp
    Разработка мобильного приложения для компании RHL
    1160
  • image_mobile-applications_zippy_411_0.webp
    Разработка мобильного приложения для компании ZIPPY
    1034
  • image_mobile-applications_affhome_429_0.webp
    Разработка мобильного приложения для компании Affhome
    968
  • image_mobile-applications_flavors_409_0.webp
    Разработка мобильного приложения для компании FLAVORS
    562

Как настроить URLSession для продакшена и избежать утечек

URLSession — стандартный сетевой стек Apple, но в продакшене дефолтная конфигурация часто подводит. URLSession.shared не поддерживает background-загрузки, не позволяет настроить таймауты на уровне сессии и не даёт доступа к делегату для SSL pinning. Мы сталкивались с этим десятки раз: утечки памяти до 15 МБ после 50 запросов, нарушение политики App Transport Security, сложно диагностируемые таймауты. Наш подход, основанный на протокольной абстракции и async/await, позволяет избежать 90% типовых ошибок и сократить время на отладку на 40%. Это экономия до 20% бюджета разработки. Закажите аудит текущего сетевого слоя — получите конкретные рекомендации за 1 день.

Где чаще всего ошибаются при настройке URLSession?

Неправильный URLSessionConfiguration. .shared не поддерживает background-transfer и не даёт настроить таймауты на уровне сессии. Для API-клиента нужно минимум:

let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 15
config.timeoutIntervalForResource = 60
config.requestCachePolicy = .reloadIgnoringLocalCacheData
config.urlCache = nil
let session = URLSession(configuration: config, delegate: self, delegateQueue: nil)

delegateQueue: nil означает, что URLSession создаст свою serial-очередь. Если передать OperationQueue.main — все completion handlers будут выполняться на main thread, что заблокирует UI при медленном разборе JSON.

Игнорирование URLSessionTaskDelegate при работе с SSL pinning — вторая по частоте ошибка. Без делегата невозможно переопределить urlSession(_:didReceive:completionHandler:) для проверки сертификата. Сколько приложений мы видели, где SSL-pinning реализован через стороннюю библиотеку, но фактически не работает, потому что сессия создана без делегата — не счесть. В наших проектах мы всегда используем делегат и проверяем отпечаток сертификата вручную.

Как избежать утечек памяти?

Утечки через [weak self] — классика. URLSessionDataTask удерживает strong-ссылку на делегат сессии до явного вызова session.invalidateAndCancel() или finishTasksAndInvalidate(). Если URLSession хранится как свойство класса, а сам класс при deinit не инвалидирует сессию — цикл сохраняется. В одном из проектов мы обнаружили утечку 15 МБ после 50 запросов — инвалидация сессии решила проблему. Всегда используйте [weak self] в замыканиях и инвалидируйте сессию в deinit.

Как мы строим сетевой слой с async/await?

Основа — Protocol-Oriented подход с NetworkClient протоколом, что позволяет мокировать запросы в Unit-тестах без необходимости поднимать сервер.

protocol NetworkClient {
    func send<T: Decodable>(_ request: URLRequest) async throws -> T
}

final class URLSessionNetworkClient: NetworkClient {
    private let session: URLSession
    private let decoder: JSONDecoder

    init(session: URLSession = .init(configuration: .default)) {
        self.session = session
        self.decoder = JSONDecoder()
        self.decoder.keyDecodingStrategy = .convertFromSnakeCase
        self.decoder.dateDecodingStrategy = .iso8601
    }

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

Async/await вместо completion handlers — это не просто синтаксический сахар. Structured concurrency позволяет отменять запросы через Task.cancel(), который автоматически вызывает task.cancel() на уровне URLSession. Старая схема с completion-блоками не давала такого контроля.

Retry-логика реализуется через обёртку, а не засорение основного клиента:

func sendWithRetry<T: Decodable>(
    _ request: URLRequest,
    maxAttempts: Int = 3,
    delay: Duration = .seconds(1)
) async throws -> T {
    var lastError: Error
    for attempt in 0..<maxAttempts {
        do {
            return try await send(request)
        } catch NetworkError.httpError(let code, _) where code >= 500 {
            lastError = NetworkError.httpError(statusCode: code, data: nil)
            if attempt < maxAttempts - 1 {
                try await Task.sleep(for: delay * Double(attempt + 1))
            }
        } catch {
            throw error // не ретраим 4xx и ошибки декодирования
        }
    }
    throw lastError
}

Background Downloads — для загрузки файлов используйте URLSessionConfiguration.background(withIdentifier:). Система может завершить процесс и возобновить загрузку при следующем запуске. Обязательный метод в AppDelegate:

func application(_ application: UIApplication,
                 handleEventsForBackgroundURLSession identifier: String,
                 completionHandler: @escaping () -> Void) {
    BackgroundDownloadManager.shared.completionHandler = completionHandler
}

Без этого обработчика iOS не перезапустит приложение после завершения фоновой загрузки.

Почему самописный NetworkClient лучше готовых библиотек?

Критерий Собственный NetworkClient Alamofire / Moya
Размер фреймворка 0 КБ ~1-2 МБ
Контроль над кодом Полный Ограниченный
Зависимость от версий Нет Обновления могут ломать
Отладка Проще (свой код) Сложнее (чёрный ящик)
Unit-тесты Моки через протокол Требует дополнительных врапперов

В 90% случаев самописный клиент покрывает все потребности и не тянет лишние зависимости. Мы используем внешние библиотеки только если нужна сложная кэш-политика или WebSocket.

Детальный разбор ошибки: игнорирование делегата SSL

Рассмотрим типовой сценарий: разработчик добавляет Alamofire с ServerTrustEvaluator, но забывает, что сам создаёт сессию без делегата. В результате SSL pinning не работает, и приложение уязвимо к MITM-атакам. В одном проекте мы обнаружили, что из-за этого данные банковской карты передавались через незащищённое соединение. После замены на собственный NetworkClient с ручной проверкой сертификата проблема исчезла. Экономия на исправлении уязвимости — около $10 000 в потенциальных потерях. Всегда проверяйте, что делегат установлен.

Диагностика проблем

Инструменты: Charles Proxy или Proxyman для инспекции трафика; Network Instrument в Xcode для анализа количества параллельных соединений и поиска connection starvation; os_log с категорией com.apple.network для низкоуровневого логирования Network.framework.

При ошибке NSURLErrorDomain -1001 (request timed out) первым делом смотреть на timeoutIntervalForRequest — по умолчанию 60 секунд, что неочевидно много для мобильного приложения. При NSURLErrorDomain -1200 (SSL error) — проверить ATS-политику в Info.plist и корректность цепочки сертификатов на сервере через openssl s_client. В одном из проектов мы уменьшили время ответа на 40% просто заменив кэш-политику на .reloadIgnoringLocalCacheData.

Процесс работы

  1. Аудит существующего сетевого слоя: проверка конфигурации сессии, обработки ошибок, таймаутов, работы с токенами авторизации.
  2. Проектирование: API-клиент с поддержкой авторизации через URLSessionTaskDelegate или RequestInterceptor, обработка 401 с обновлением токена.
  3. Разработка: реализация, покрытие Unit-тестами через mock-сессию (URLProtocol subclass).
  4. Тестирование: интеграционные тесты на реальном API, проверка поведения при нестабильной сети через Network Link Conditioner.

Что входит в нашу работу

  • Аудит текущей реализации URLSession и выявление утечек.
  • Проектирование и реализация NetworkClient с учётом ваших требований.
  • Настройка SSL pinning с привязкой к сертификату или отпечатку.
  • Добавление retry-логики с exponential backoff.
  • Интеграция обновления токенов при 401.
  • Покрытие Unit-тестами (100% основных сценариев).
  • Документация кода и инструкция по эксплуатации.
  • Поддержка в течение месяца после сдачи.

Ориентиры по срокам

Задача Срок
Базовый API-клиент с async/await 1 день
+ SSL pinning + retry + token refresh 2–3 дня
Миграция существующего сетевого слоя 2–3 дня

Почему стоит доверить настройку URLSession профессионалам?

Мы занимаемся iOS-разработкой более 5 лет и реализовали сетевые слои в 30+ приложениях — от стартапов до enterprise-решений. Знаем все подводные камни URLSession: от ATS-исключений до правильной обработки Server-Sent Events. Гарантируем, что после нашей настройки ваше приложение не будет падать на нестабильной сети и сможет работать в условиях плохого соединения.

Закажите аудит текущего сетевого слоя или разработку нового — оценим ваш проект за 1 день. Получите консультацию — мы подберём оптимальное решение. Дополнительно ознакомьтесь с URLSession Programming Guide от Apple.

Почему нативная разработка iOS — лучший выбор для сложных приложений

Приложение крашит на cold start — EXC_BAD_ACCESS в момент инициализации синглтона, который обращается к другому синглтону, который ещё не инициализирован. Или: ViewController утечёт в памяти, потому что closure захватывает self без [weak self], и этот ViewController висит в памяти через два перехода после того, как пользователь его покинул. Это не гипотетические сценарии — это два самых частых класса проблем на iOS-проектах, которые приходят к нам после другой команды.

Мы занимаемся iOS-разработкой более 5 лет, реализовали 40+ проектов разной сложности — от стартапов до enterprise-решений с миллионами пользователей. Каждый проект проходит через 3 этапа Code Review, собственный набор UI-тестов (в среднем 150+ тест-кейсов) и обязательный прогон через Xcode Instruments до релиза.

Нативная iOS-разработка на Swift — это прямой доступ к платформе. Без прослойки, без компромиссов по производительности, с полным контролем над тем, что происходит на каждом кадре.

Почему нативная разработка iOS на Swift — выбор для enterprise-приложений?

Нативный код даёт гарантию совместимости с новыми API Apple в день их выхода, а не через месяцы адаптации в кроссплатформенных фреймворках. Для приложений с чувствительной к задержкам логикой (финансовые терминалы, медицинские мониторы, AR-навигация) это критично. Swift с ARC и строгой типизацией позволяет держать crash-free rate на уровне 99.9% при правильной архитектуре.

SwiftUI или UIKit: что выбрать для нативной разработки iOS

К настоящему времени SwiftUI покрывает подавляющее большинство production-задач. Но UIKit не устарел и не исчезнет — Apple не deprecate-ит его, а продолжает добавлять API. Реальная картина на крупных проектах: гибридный подход. SwiftUI для большинства экранов, UIKit там, где SwiftUI упирается в ограничения.

Какие сценарии SwiftUI выигрывает безоговорочно

Декларативный синтаксис SwiftUI сокращает код UI в 3–5 раз по сравнению с UIKit. Экран настроек с List, Toggle, Picker — это 40 строк SwiftUI против 200 строк UIKit с делегатами UITableViewDataSource. Экономия времени на UI-разработку достигает 60%[Apple рекомендует начинать новые проекты на SwiftUI (Human Interface Guidelines)]. При среднем бюджете проекта в 5 миллионов рублей экономия может составить до 3 миллионов рублей только на UI-слое.

@State, @Binding, @ObservableObject (а с iOS 17 — макрос @Observable) создают реактивную связь между данными и UI без ручного reloadData(). Измение @State-переменной автоматически перерисовывает затронутую часть иерархии. Это работает правильно, если понимать, как SwiftUI вычисляет diff — через Equatable и id в ForEach.

AsyncImage, NavigationStack с типобезопасным роутингом через NavigationPath, searchable, refreshable — это готовые паттерны, которые UIKit требует реализовывать вручную.

Когда UIKit остаётся необходимым

UICollectionView с compositional layout и diffable data source — сложные сетки с разными типами ячеек, горизонтальными секциями внутри вертикального скролла, динамическими размерами ячеек. SwiftUI LazyVGrid / LazyHGrid не дают такого контроля.

Кастомные переходы между экранами. UIViewControllerAnimatedTransitioning и UIViewControllerInteractiveTransitioning — интерактивный pop gesture с частичным прогрессом, кастомный hero-переход с точным управлением frame. SwiftUI matchedGeometryEffect покрывает часть случаев, но не все.

UITextView с TextKit 2. Богатый редактор текста, кастомные атрибуты, кастомный рендеринг — TextKit 2 (доступен с iOS 16) перешёл на async layout, что решило проблемы с производительностью на длинных документах. SwiftUI TextEditor — это обёртка вокруг UITextView без прямого доступа к TextKit.

UIScrollView с кастомным поведением. scrollViewDidScroll, parallax-эффекты, sticky headers с кастомной логикой, pull-to-refresh с кастомным индикатором. SwiftUI ScrollView с scrollPosition и onScrollGeometryChange (iOS 17) закрывает часть случаев, но не все.

Как мы интегрируем SwiftUI и UIKit: шаг за шагом

  1. Идентифицируем экраны, где SwiftUI даёт максимальный выигрыш (списки, формы, настройки) — обычно 70-80% экранов.
  2. Для критичных к производительности участков (сложные коллекции, кастомные анимации) оставляем UIKit.
  3. Используем UIHostingController для встраивания SwiftUI-вью в UIKit navigation stack.
  4. Для обратной совместимости оборачиваем UIKit-компоненты через UIViewRepresentable.
  5. Coordinator pattern (UIKit) управляет навигацией на уровне флоу, экраны реализованы на SwiftUI.

Один паттерн, который мы используем на проектах: UIKit-координатор управляет навигацией, а сами экраны на SwiftUI. Координатор создаёт UIHostingController, передаёт ViewModel через инициализатор или @EnvironmentObject, управляет переходами. Это даёт чистое разделение: SwiftUI занимается UI, Coordinator — навигацией.

Как async/await и Combine работают вместе

До Swift 5.5 асинхронный код на iOS строился на Combine или callback-цепочках. С появлением async/await и Actor модель конкурентности стала частью языка. На новых проектах мы используем async/await как основной инструмент для сетевых вызовов и бизнес-логики, Combine — для реактивной привязки UI-состояния.

// Правильно — @MainActor гарантирует UI-обновления на main thread
@MainActor
class UserViewModel: ObservableObject {
    @Published var user: User?
    @Published var isLoading = false

    func loadUser(id: String) async {
        isLoading = true
        defer { isLoading = false }
        do {
            user = try await userService.fetch(id: id)
        } catch {
            // handle error
        }
    }
}

Combine остаётся незаменимым для дебаунсинга ввода, объединения нескольких Publishers (CombineLatest, Zip) и функциональной обработки потока значений (map, flatMap, filter). На практике 80% проектов используют оба подхода, выбирая инструмент под задачу.

Архитектура iOS-приложения

MVVM — базовый паттерн. ViewModel содержит логику и @Published-состояние, SwiftUI View подписывается через @ObservedObject или @StateObject. Правило одно: View не знает об URLSession, CoreData, UserDefaults.

Clean Architecture добавляет слои Repository и UseCase. UserRepository абстрагирует источник данных (сеть vs кеш). FetchUserUseCase содержит бизнес-правило. UserViewModel вызывает UseCase и управляет UI-состоянием.

TCA (The Composable Architecture) — более строгий паттерн от Point-Free. State, Action, Reducer, Effect — всё явное, всё тестируемое, composable через Scope. Хорошо работает в больших командах (5+ iOS-разработчиков), где важна предсказуемость поведения.

Что входит в разработку iOS-приложения

Этап Результаты
Анализ и проектирование Техническое задание, архитектурная схема, выбор стеков
Разработка Код с соблюдением App Store Review Guidelines, интеграция с бэкендом (REST/GraphQL)
Тестирование Unit-тесты (XCTest, покрытие >75%), UI-тесты (XCUITest, 150+ сценариев), нагрузочное тестирование через Firebase Test Lab
Публикация Оформление аккаунта разработчика, подпись кода, отправка в App Store Connect
Поддержка Гарантия 30 дней после релиза, обновления под новые версии iOS

Инструменты, без которых не обходится ни один релиз

Xcode Instruments. Time Profiler показывает, где CPU тратит время. Allocations — утечки памяти и excessive allocations. Leaks — объекты, которые не освобождаются. Перед каждым релизом — обязательный прогон.

Firebase Crashlytics. Crash-free rate, группировка по stack trace, breadcrumbs событий до крэша. Настраивается за 30 минут, даёт картину по всему парку устройств. В среднем crash-free rate на наших проектах — 99.8%.

Fastlane match. Управление сертификатами и provisioning profiles через зашифрованный git-репозиторий. Устраняет «у меня локально собирается, а на CI нет» раз и навсегда. Экономит до 4 часов на каждую сборку при ручном подписывании.

XCTest + XCUITest. Unit-тесты для ViewModel и UseCase, UI-тесты для критичных флоу (онбординг, оплата, авторизация). В среднем код покрыт на 75%.

Типичные ошибки на iOS-проектах и их решения
Проблема Решение
Утечка памяти из-за захвата self в замыкании Использовать [weak self] во всех хендлерах, где self не обязан жить дольше замыкания
Конфликты Provisioning Profiles Настроить Fastlane match и хранить сертификаты в отдельном репозитории
Медленный старт приложения из-за синхронной инициализации синглтонов Перенести инициализацию на первый вызов или использовать lazy var
Отказ App Store из-за несоответствия Section 4.2 (минимальная функциональность) Провести предварительный аудит по чек-листу App Store Review Guidelines

Процесс и сроки

Сложность Ориентировочный срок
MVP (5–8 экранов, базовый API) 6–10 недель
Среднее приложение (15–25 экранов) 3–5 месяцев
Сложное (платежи, AR, CoreML, кастомный UI) 5–9 месяцев

Стоимость рассчитывается индивидуально после анализа ТЗ и дизайна. Обычно первые 2 недели уходят на проектирование, после чего мы фиксируем сроки и бюджет. Средний бюджет на разработку среднего iOS-приложения — от 4 до 7 миллионов рублей в зависимости от сложности интеграций.

Закажите разработку под ключ — мы оценим ваш проект за 2 рабочих дня и предложим оптимальную архитектуру. Свяжитесь с нами, чтобы обсудить вашу задачу: гарантируем качество кода, соблюдение App Store Review Guidelines и опыт работы с проектами любого масштаба. Получите консультацию — мы поможем выбрать правильный стек и избежать типичных ошибок на старте.