🐥 Swift 6 краши при многопоточности
Если вам кажется что Swift 6 strict concurrency - это просто больше compile-time проверок - это не так. Часть проблем ловится уже в runtime, причём иногда прямо в production. Даже если проект собирается без warnings, Swift может вставить dynamic isolation checks на границах акторов и GCD. И если код ожидал один execution context, а приехал в другой — приложение упадет 😥. Чаще всего в crash reports это выглядит так:
_dispatch_assert_queue_fail
_swift_task_checkIsolatedSwift
Суть проблемы простая: closure или метод унаследовал actor isolation там, где был объявлен, а вызвали его потом из другого потока. Например, closure внутри @MainActor context может неявно стать main-actor-isolated. А потом вы передаёте её в context.perform у Core Data, который выполняет блок на своей private queue.
Где это особенно легко поймать:
🟢Core Data context.perform из @MainActor-кода
🟢Combine pipeline, где operator выполняется до receive(on:)
🟢NotificationCenter publisher, если notification прилетает с background thread
🟢delegate callbacks от SDK, которые не обещают main thread
🟢MainActor.assumeIsolated, если вы на самом деле не на MainActor
🟢actor methods, где после await состояние уже могло измениться из-за reentrancy
Практические правила:
🟢если Combine-operator должен работать на main — ставьте .receive(on: DispatchQueue.main) до него, а не после
🟢если closure не должен наследовать actor isolation — явно помечайте его @Sendable
🟢если delegate может прийти не с main thread — делайте метод nonisolated, а UI-работу отправляйте в Task { @MainActor in ... }
🟢не используйте MainActor.assumeIsolated как красивую замену await MainActor.run
🟢не рассчитывайте, что @MainActor на всём классе автоматически безопасен для всех callback’ов
Swift 6 начал громко показывать то, что раньше было скрытой проблемой:
🟡closure наследует isolation от места объявления, а не от места выполнения
🟡receive(on:) не спасает код, который уже выполнился до него
🟡delegate от системного SDK не обязан приходить на main thread
🟡билд без ворнингов не гарантирует отсутствие рантайм крашей из-за многопоточности
🟡после await внутри actor нельзя слепо доверять старому состоянию
Swift 6 concurrency — это не только про то, чтобы пофиксить warnings, это про пересмотр границ - где выполняется код, кто владеет состоянием и какой thread/actor реально вызывает callback.
🐥 Actors vs Queues vs Locks в Swift
Статья достойная добавления в закладки, все чётко и по-полочкам про то как защитить shared state, плюсы и минусы решений, все то что полезно понимать не только для собеседований
➡️actor — это защита состояния на уровне языка
➡️DispatchQueue — это ручная сериализация выполнения
➡️Lock — это низкоуровневый замок вокруг маленького участка кода
Все три решают похожую проблему, но совершенно разной ценой. Где хорошо заходят actors:
🟢когда код уже живёт в Swift Concurrency
🟢когда состояние логически принадлежит отдельной сущности
🟢когда хочется, чтобы компилятор помогал с isolation и Sendable
🟢когда нормальная async-модель доступа через await
Actors хороши не потому, что они новые. а потому что они делают границы доступа явными.
Но есть нюанс: await внутри actor-метода — это точка, где атомарность ломается. Actor защищает от одновременного доступа к состоянию, но не отменяет «перезаходы» и не превращает логику в транзакцию
Где всё ещё уместны queues:
🟢когда есть legacy-код на GCD
🟢когда нужно аккуратно сериализовать работу без полной миграции на async/await
🟢когда вокруг много callback-based API
🟢когда уже есть понятная очередь на подсистему: database queue, storage queue, parsing queue
DispatchQueue — не старый плохой способ, а просто это инструмент, где порядок, блокировки и переходы между очередями вы контролируете сами. А значит, сами же отвечаете за дедлоки, лишние переключения контекстов и внезапную сложность.
Где могут выигрывают locks:
🟢когда нужен очень маленькая синхронизация в критической секции
🟢когда доступ должен быть мгновенным, без await
🟢когда вы защищаете простое значение или короткую операцию
🟢когда actor был бы архитектурно тяжелее самой проблемы
Но locks — это как раз тот случай, где просто заверну в NSLock легко превращается в мину:
🟡у компилятора почти нет понимания вашей синхронизации
🟡нельзя держать lock через await
🟡длинный critical section быстро портит производительность
🟡неправильный порядок lock’ов-привет, deadlock
🟣Actors — хороший дефолт для нового async-кода.
🟣Queues — практичный мост для существующей архитектуры.
🟣Locks — точечный инструмент для маленьких синхронных участков.
Не нужно заменять всё на actors только потому, что Swift 6 стал строже. Не нужно тащить locks туда, где на самом деле нужна нормальная модель владения и изоляции.
📱 Background Refresh в SwiftUI
На iOS запустить что либо точно когда хочется не получится, но вот попросить систему и добиться ожидаемого поведения в большинстве случаев возможно. Для этого есть BGAppRefreshTaskRequest — это не cron, а просьба к системе: разбуди приложение не раньше указанного времени, если посчитаешь это уместным.
Но перед этим всё равно нужно пройти классический набор:
🟢включить Background Modes в Capabilities
🟢добавить Background fetch
🟢зарегистрировать task identifier в Info.plist
🟢запланировать BGAppRefreshTaskRequest
🟢повесить handler через .backgroundTask
Но у этого способа есть целый набор юзкейсов:
🟢обновить кеш до следующего запуска приложения
🟢подтянуть свежий контент для первого экрана
🟢сделать так, чтобы пользователь чаще видел готовые данные, а не спиннер
🟢аккуратно обслуживать локальные данные без явного открытия приложения
На практике это хороший паттерн для приложений с ежедневным или периодическим контентом:
при запуске показываем кеш, при необходимости обновляем данные, после успешного обновления планируем background refresh на следующий раз. Но важно не перепутать инструмент с гарантией:
🟡earliestBeginDate не гарантирует запуск в конкретное время
🟡система может вообще не дать фонового окна
🟡нельзя завязывать на это критичную бизнес-логику
🟡фоновой задаче нужно уложиться примерно в 30 секунд
🟡если регулярно не укладываться, iOS может начать реже давать такие запуски
Для долгих сетевых операций нужен background URLSession.
Для тяжёлой обработки — BGProcessingTaskRequest. Background Refresh — это не способ выполнять работу «когда захотим», а способ подготовить приложение к следующему открытию.
🎯 Apple случайно оставили Claude.md файлы в сегодняшнем апдейте приложения Apple Support
Интереснейшее обсуждение на реддите: Claude.md обычно содержит:
🟢правила для AI-ассистента по проекту
🟢coding style
🟢архитектурный контекст
🟢команды сборки/тестов
🟢ограничения и known issues
То есть это не Apple написала приложение полностью нейросетью, а скорее сигнал, что внутри компании начали использовать продукты Антропика. Сами файлы базовые и никаких инсайтов не содержат, на мой взгляд
🐥 Immediate Tasks в Swift Concurrency
Сначала кажется, что это просто ещё один новый API в Swift 6.2. А потом понимаешь, что Task.immediate это уже не про синтаксис, а про момент старта async-работы. Обычный Task создаётся и ставится в очередь на выполнение позже.
Task.immediate начинает выполняться сразу в текущем execution context, пока не упрётся в первый настоящий suspension point.
То есть разница маленькая по формулировке, но вполне реальная по последствиям.
Где это полезно:
🟢когда у вас короткая операция и лишняя задержка на scheduling уже чувствуется
🟢когда код уже находится на нужном actor и не хочется лишнего перехода
🟢когда важно сделать старт async-операции максимально предсказуемым
На практике это:
🟢меньше лишней задержки на запуск задачи
🟢больше контроля в performance-sensitive местах
🟢более точное поведение в некоторых UI и concurrency-сценариях
Но это как раз тот случай, где новая возможность легко превращается в красивую переоптимизацию:
🟡Task.immediate не должен становиться новым дефолтом вместо обычного Task
🟡если у вас нет измеримой проблемы, скорее всего обычного Task достаточно
🟡чем тоньше контроль над concurrency, тем выше шанс усложнить код без реальной пользы
🈸 Apple добавила рассрочку для годовой подписки
Я сначала не понял а потом как понял. Apple анонсировала новый вариант подписки: теперь можно продавать подписку с ежемесячной оплатой, но с обязательством на 12 месяцев. Это уже можно настраивать в App Store Connect и тестировать в Xcode:
🟢можно сделать оффер дешевле на год вперед со скидкой
🟢появляется новый формат для A/B‑тестов подписок и стратегии ценовой политики
🟢придётся аккуратно объяснять commitment в UI, иначе легко словить недоверие и просадку в конверсии и отмену подписки через оспаривание
Что важно помнить:
🟡пользователь платит каждый месяц, а не сразу за год
🟡отменить можно в любой момент, но подписка не продлится только после выполнения уже взятых платежей
🟡Apple будет показывать, сколько платежей уже прошло и сколько осталось
🟡для пользователей заработает с выходом iOS 26.5 / iPadOS 26.5 / macOS 26.5 / tvOS 26.5 / visionOS 26.5 в мае
Иногда проблема не в самой цене, а в том, как именно пользователь понимает условия подписки, новая модель удобна как для пользователей (платят меньше) так и для разработчиков (позволяет точнее предсказывать денежные поступления)