Материал подготовлен для будущих студентов специализации "iOS Developer".
Swift 6 вводит более строгие проверки изоляции конкурентности и поддерживает поэтапную миграцию, модуль за модулем. Хотя рекомендуемая Apple стратегия выглядит мягкой, на практике вы можете столкнуться со скрытыми сбоями во время выполнения, особенно когда в проекте одновременно сосуществуют модули на Swift 5 и Swift 6.
В этой статье разберем два реальных кейса, на которых команды регулярно спотыкаются при поэтапной миграции на Swift 6. Мы свяжем их с исходниками Swift Runtime, объясним задумку и триггеры падений, и завершим практическими мерами по снижению рисков и рекомендациями по обновлению.
TL;DR
Swift 6 добавляет проверки изоляции (
swift_task_checkIsolated), которые по умолчанию приводят к падению во время выполнения, чтобы предотвратить гонки данных.Когда модуль на Swift 6 вызывает модуль на Swift 5 и передаёт замыкание с
@escapingбез@Sendable, проверка изоляции может завершить приложение сбоем во время выполнения, причём без диагностики на этапе компиляции.@preconcurrencyвлияет только на межмодульные проверки во время компиляции; он не отключает проверки изоляции во время выполнения в Swift 6. Смешанный граф Swift 5/6 остаётся рискованным и может падать во время выполнения.Рекомендация: обновляться снизу вверх и как можно раньше помечать критичные замыкания с
@escapingкак@Sendable.Мы приводим анализ на уровне исходников, показывающий, как именно возникает падение, и как переопределить поведение через переменную окружения (только для отладки).
Обзор стратегии миграции: как спланировать поэтапный переход на Swift 6
Согласно документации по миграции Swift, Swift 6 можно внедрять по модулям. Разные модули могут двигаться параллельно. Рабочий поэтапный план выглядит так:
Установить для модуля
SWIFT_STRICT_CONCURRENCY = completeи исправить ошибки компиляции.Оставить
completeи постепенно устранить предупреждения, связанные с миграцией.Переключить
SWIFT_VERSIONэтого модуля на 6, чтобы включить режим Swift 6.Повторять цикл, пока все модули не будут перенесены.
Раннее предупреждение: когда WebKit встречается с Complete Strict Concurrency
Пример
Предположим, вы давно поддерживаете и поставляете в релиз следующий код:
import UIKit import WebKit class ViewController: UIViewController, WKNavigationDelegate { private var webView: WKWebView! override func viewDidLoad() { super.viewDidLoad() webView = WKWebView(frame: view.bounds) webView.navigationDelegate = self webView.load(URLRequest(url: URL(string: "<https://www.apple.com>")!)) view.addSubview(webView) } func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { // ваша бизнес-логика print("decidePolicyFor navigation action") decisionHandler(.allow) } func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) { // ваша бизнес-логика print("decidePolicyFor navigation response") decisionHandler(.allow) } }
Если установить SWIFT_STRICT_CONCURRENCY = Complete, проект соберётся без ошибок и будет нормально запускаться.
В Xcode 15 всё по-прежнему выглядит нормально. Но после обновления до Xcode 16 вы увидите предупреждение вроде такого:
Instance method 'webView(_:decidePolicyFor:decisionHandler:)' nearly matches optional requirement 'webView(_:decidePolicyFor:decisionHandler:)' of protocol 'WKNavigationDelegate'
Если проигнорировать это предупреждение, метод делегата может перестать корректно вызываться во время выполнения, что приведёт к функциональным регрессиям или багам в продакшене.
Что произошло?
Сравните определения WKNavigationDelegate:
/// до iOS 18 public protocol WKNavigationDelegate : NSObjectProtocol { @available(iOS 8.0, *) optional func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) } /// iOS 18 @MainActor public protocol WKNavigationDelegate : NSObjectProtocol { @available(iOS 8.0, *) optional func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping @MainActor @Sendable (WKNavigationActionPolicy) -> Void) @available(iOS 8.0, *) optional func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction) async -> WKNavigationActionPolicy }
Начиная с iOS 18 WebKit добавил @MainActor (и @Sendable для обработчика), из-за чего ваша существующая реализация больше не совпадает с сигнатурой протокола.
Для средних и больших кодовых баз перевод Strict Concurrency в режим Complete задуман как способ «подсветить» предупреждения, связанные со Swift 6, чтобы их можно было исправлять постепенно. ⚠️ Эти предупреждения могут выглядеть не связанными со Swift 6, но «взрываются» на более строгих проверках Swift 6. Если их игнорировать, появляется скрытый риск.
Исправление
// Swift 5/6; минимальная версия iOS — 16; собирается в Xcode 15 и 16. // Ключевой момент: реализовать async-вариант для iOS 18+; сохранить старую сигнатуру для более старых ОС // и пометить её как устаревшую начиная с iOS 18, чтобы избежать «почти совпадает». import UIKit import WebKit final class ViewController: UIViewController, WKNavigationDelegate { private var webView: WKWebView! override func viewDidLoad() { super.viewDidLoad() webView = WKWebView(frame: view.bounds) webView.navigationDelegate = self view.addSubview(webView) webView.load(URLRequest(url: URL(string: "<https://www.apple.com>")!)) } // iOS 18+: async-вариант @available(iOS 18.0, *) func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction) async -> WKNavigationActionPolicy { // бизнес-логика (MainActor) return .allow } // Старая сигнатура для более старых ОС; помечена как устаревшая начиная с iOS 18, чтобы избежать «почти совпадает». @available(iOS, introduced: 8.0, obsoleted: 18.0) func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { decisionHandler(.allow) } // (Рекомендуется) Сделать то же самое для navigationResponse: @available(iOS 18.0, *) func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse) async -> WKNavigationResponsePolicy { return .allow } @available(iOS, introduced: 8.0, obsoleted: 18.0) func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) { decisionHandler(.allow) } }
Вывод
Стабильность ABI ≠ совместимость исходников с SDK. Если аннотации @MainActor/@Sendable меняются у методов @objc-протоколов, нужно либо пересобраться на новом SDK, либо добавить условные реализации.
Реальный подводный камень: межмодульные асинхронные колбэки вызывают падения из-за проверок изоляции
Второй пример
После того как вы привели ModuleA в порядок под SWIFT_STRICT_CONCURRENCY = Complete (без ошибок и предупреждений), вы переключаете режим языка Swift на 6:
// ModuleA // предупреждений нет // SWIFT_STRICT_CONCURRENCY = completed // SWIFT_VERSION = 6 @preconcurrency import ModuleB class ViewController: UIViewController { override func viewDidLoad() { DataManager.save(value: "foo") { print("bar") } } } // ModuleB // предупреждений нет // SWIFT_STRICT_CONCURRENCY = completed // SWIFT_VERSION = 5 import ModuleC public final class DataManager { public static func save(value: String, completion: @escaping () -> Void) { CacheManager.save(value: value) { completion() } } } // ModuleC // SWIFT_STRICT_CONCURRENCY = completed // SWIFT_VERSION = 5 final class CacheManager { static func save(value: String, completion: @escaping () -> Void) { DispatchQueue.global().asyncAfter(deadline: .now() + 1) { completion() // Warning: Capture of 'completion' with non-sendable type '() -> Void' in a '@Sendable' closure } } }
? На iOS 18+ вы получите падение во время выполнения.

Причина
ViewController по умолчанию изолирован @MainActor; DataManager и CacheManager не изолированы. Когда включены runtime-проверки изоляции в Swift 6, если замыкание, для которого изоляция выводится/ожидается как @MainActor, выполняется не на главном исполнителе, swift_task_checkIsolated сработает как assert и приложение упадёт.
Глубокое погружение в runtime
Swift 5.10 — более мягкое поведение

Swift 6.0 — появляется swift_task_checkIsolated

Swift 6.2 — текущая версия (более тонкая логика через флаги)

Вывод: начиная со Swift 6.0, swift_task_checkIsolated используется для assert-проверки в ситуации, когда текущий исполнитель (executor) не соответствует ожидаемой изоляции. В Swift 6.2 решение уточняется с помощью флагов, но основная идея остаётся прежней.
Когда включаются assert-проверки?
Версия Swift runtime и линковка с SDK
Новый Swift Runtime (например, Swift 6 и новее) → падение по умолчанию допускается; флаг Assert включён.
Старый runtime / старое приложение без пересборки → режим совместимости; падения нет; флаг Assert выключен.
swift_task_is_current_executor_flag __swift_bincompat_useLegacyNonCrashingExecutorChecks() { swift_task_is_current_executor_flag options = swift_task_is_current_executor_flag::None; #if !SWIFT_CONCURRENCY_EMBEDDED if (!swift::runtime::bincompat:: swift_bincompat_useLegacyNonCrashingExecutorChecks()) { options = swift_task_is_current_executor_flag( options | swift_task_is_current_executor_flag::Assert); } #endif return options; } bool swift_bincompat_useLegacyNonCrashingExecutorChecks() { #if BINARY_COMPATIBILITY_APPLE switch (isAppAtLeastFall2024()) { case oldOS: return true; // Режим совместимости для старых версий ОС case oldApp: return true; // Режим совместимости для старых приложений case newApp: return false; // Новое поведение для новых приложений } #else return false; // Всегда использовать новое поведение на не-Apple платформах #endif }
Переопределение через переменную окружения
Поведение «падать / не падать» можно переопределить через
SWIFT_IS_CURRENT_EXECUTOR_LEGACY_MODE_OVERRIDE(внутри runtime читается черезconcurrencyIsCurrentExecutorLegacyModeOverride).Значения:
crash/swift6(включить Assert) илиnocrash/legacy(выключить Assert).
static void swift_task_setDefaultExecutorCheckingFlags(void *context) { auto *options = static_cast<swift_task_is_current_executor_flag *>(context); auto modeOverride = swift_bincompat_selectDefaultIsCurrentExecutorCheckingMode(); if (modeOverride != swift_task_is_current_executor_flag::None) { *options = modeOverride; } } if (const char *modeStr = __swift_runtime_env_useLegacyNonCrashingExecutorChecks()) { if (strcmp(modeStr, "nocrash") == 0 || strcmp(modeStr, "legacy") == 0) { options = swift_task_is_current_executor_flag( options & ~swift_task_is_current_executor_flag::Assert); } else if (strcmp(modeStr, "crash") == 0 || strcmp(modeStr, "swift6") == 0) { options = swift_task_is_current_executor_flag( options | swift_task_is_current_executor_flag::Assert); } }
Итог:
Источник |
Поведение |
SDK ≥ «Fall 2024» |
Assert включён → возможно падение |
Переопределение через переменную окружения |
Можно отключить Assert для отладки |
Старый SDK / приложение |
Режим совместимости по умолчанию → падения нет |
Обходной путь через переменную окружения (только для отладки)
Имя:
SWIFT_IS_CURRENT_EXECUTOR_LEGACY_MODE_OVERRIDEЗначение:
legacy(илиnocrash); используйтеcrash/swift6, чтобы принудительно включить Assert.Где задаётся: Xcode ▸ Scheme ▸ Run ▸ Environment Variables.
Это переопределяет решение о флаге Assert. Полезно для диагностики и переходного периода, но не для продакшена.
Исправление
Добавьте @Sendable к замыканиям с @escaping в ModuleB и ModuleC. (При желании можно также объявить @MainActor, но в этой статье фокус только на @Sendable)
// Module B // SWIFT_STRICT_CONCURRENCY = completed // SWIFT_VERSION = 5 public final class DataManager { public static func save(value: String, completion: @escaping @Sendable () -> Void) { CacheManager.save(value: value) { completion() } } } // Module C // SWIFT_STRICT_CONCURRENCY = completed // SWIFT_VERSION = 5 final class CacheManager { static func save(value: String, completion: @escaping @Sendable () -> Void) { DispatchQueue.global().asyncAfter(deadline: .now() + 1) { completion() } } }
На практике ModuleC можно не трогать: часто достаточно добавить @Sendable к публичному колбэку в ModuleB. Альтернативный вариант: перевести ModuleB на Swift 6 (Language Mode), это тоже избавляет от падения.
Наблюдаемая матрица:
Модуль A |
Модуль B |
Модуль C |
Результат |
|---|---|---|---|
6 |
5 |
5 |
? |
6 |
6 |
5 |
✅ |
6 |
5 |
6 |
? |
6 |
6 |
6 |
✅ |
Когда ModuleC обновлён до Swift 6, это остаётся предупреждением, а не ошибкой. Тем не менее важно выявлять и исправлять такие вещи как можно раньше в разработке.
По результатам этих экспериментов наиболее осторожная стратегия обновления — снизу вверх, начиная с «листьев» графа зависимостей. Однако зависимости от сторонних библиотек всё равно могут вас заблокировать; @preconcurrency в случае этого падения не помогает.
Сторонние библиотеки
Например, Alamofire 5.10.0 адаптировался к конкурентности Swift 6: он добавляет @Sendable ко всем замыканиям с @escaping. Если вы готовитесь к миграции на Swift 6, сначала проверьте, завершили ли ваши зависимости адаптацию под конкурентность Swift 6.
// 5.9.0 func didReceiveResponse(_ response: HTTPURLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) // 5.10.0 func didReceiveResponse(_ response: HTTPURLResponse, completionHandler: @Sendable @escaping (URLSession.ResponseDisposition) -> Void)
Расширенный кейс: у Combine похожие проблемы
class ViewController: UIViewController { private var cancellables = Set<AnyCancellable>() private let viewModel = SomeViewModel() func setupBinding() { viewModel.valuesPublisher .filter { !$0.isEmpty } .receive(on: DispatchQueue.main) .sink { values in print("Received values: \\(values)") } .store(in: &cancellables) } }
Если замыкание в filter определено в модуле на Swift 6, а publisher испускает значения не на главном потоке, вы всё равно можете получить падение из-за конфликта изоляции.
Итоги и рекомендации
Swift 6 добавляет проверки изоляции во время выполнения для обеспечения потокобезопасности, но смешанный граф модулей Swift 5/6 усложняет выявление проблем и повышает вероятность падений во время выполнения.
Компилятор не обязательно продиагностирует такие случаи в модулях на Swift 6;
@preconcurrencyне может подавить runtime-проверки.Предпочтительна миграция снизу вверх: сначала обновляйте самые глубокие зависимости.
Явно помечайте замыкания с @escaping как
@Sendable, особенно вокруг асинхронных операций и колбэков.Проведите аудит сторонних зависимостей и обновите их до последних версий.
Ссылки
[1] Swift.org – поэтапное внедрение (руководство по миграции конкурентности Swift 6)
[2] Apple Developer Forums – поломка бинарной совместимости WebKit в iOS 18
[3] Исходники Swift Runtime — Actor.cpp (Swift-5.10)
[4] Исходники Swift Runtime — Actor.cpp (swift-6.0)
[5] Исходники Swift Runtime — Actor.cpp (main)
[6] Исходники Swift Runtime — Bincompat.cpp (swift_bincompat_useLegacyNonCrashingExecutorChecks)
[7] Исходники Swift — переменная окружения SWIFT_IS_CURRENT_EXECUTOR_LEGACY_MODE_OVERRIDE
[8] Alamofire — адаптация Sendable и конкурентности

Если хочется не просто «перейти на Swift», а уверенно держать в голове язык, UIKit/SwiftUI и типовые ловушки рантайма (включая конкурентность), это удобно закрывать системной практикой. На специализации iOS Developer вы последовательно сможете развить навыки разработки под экосистему Apple и довести до готового приложения, которое можно показать как рабочий результат.
Для знакомства с форматом обучения и экспертами приходите на бесплатные демо-уроки:
17 февраля 20:00. «Храним данные в приложении правильно, SwiftData». Записаться
10 марта 20:00. «От первого HTTP-запроса к собственному сетевому слою в Swift — быстро и понятно». Записаться
18 марта 20:00. «Пишем простой проигрыватель на SwiftUI». Записаться
Еще больше бесплатных уроков от преподавателей курсов можно посмотреть в календаре мероприятий.
Ivnika
Перевод мягко говоря "машинный"...
house2008
Да тут наполовину, а может и весь текст уже устаревшая информация.
Xcode 26 вышел более полугода назад где default actor является Main, поэтому это всё уже будет изолированно и ошибки не будет. Остальные кейсы не проверял, но думаю поведение в Xcode 26 другое чем описывается в статье. А, оно и понятно, статья перевод и выложил редактор, а не специалист про профилю статьи.