Материал подготовлен для будущих студентов специализации "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 можно внедрять по модулям. Разные модули могут двигаться параллельно. Рабочий поэтапный план выглядит так:

  1. Установить для модуля SWIFT_STRICT_CONCURRENCY = complete и исправить ошибки компиляции.

  2. Оставить complete и постепенно устранить предупреждения, связанные с миграцией.

  3. Переключить SWIFT_VERSION этого модуля на 6, чтобы включить режим Swift 6.

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

Раннее предупреждение: когда 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+ вы получите падение во время выполнения.

crash_stack
crash_stack

Причина

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-проверки?

  1. Версия 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
}
  1. Переопределение через переменную окружения

  • Поведение «падать / не падать» можно переопределить через 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 испускает значения не на главном потоке, вы всё равно можете получить падение из-за конфликта изоляции.

Итоги и рекомендации

  1. Swift 6 добавляет проверки изоляции во время выполнения для обеспечения потокобезопасности, но смешанный граф модулей Swift 5/6 усложняет выявление проблем и повышает вероятность падений во время выполнения.

  2. Компилятор не обязательно продиагностирует такие случаи в модулях на Swift 6; @preconcurrency не может подавить runtime-проверки.

  3. Предпочтительна миграция снизу вверх: сначала обновляйте самые глубокие зависимости.

  4. Явно помечайте замыкания с @escaping как @Sendable, особенно вокруг асинхронных операций и колбэков.

  5. Проведите аудит сторонних зависимостей и обновите их до последних версий.

Ссылки

[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». Записаться

Еще больше бесплатных уроков от преподавателей курсов можно посмотреть в календаре мероприятий.

Комментарии (2)


  1. Ivnika
    13.02.2026 14:48

    Перевод мягко говоря "машинный"...


    1. house2008
      13.02.2026 14:48

      Да тут наполовину, а может и весь текст уже устаревшая информация.

      ViewController по умолчанию изолирован @MainActorDataManager и CacheManager не изолированы.

      Xcode 26 вышел более полугода назад где default actor является Main, поэтому это всё уже будет изолированно и ошибки не будет. Остальные кейсы не проверял, но думаю поведение в Xcode 26 другое чем описывается в статье. А, оно и понятно, статья перевод и выложил редактор, а не специалист про профилю статьи.