iOS SDK: выживание при приостановке
Платформа CoreLocation от Apple предоставляет мощные инструменты для фонового отслеживания, но iOS чрезвычайно агрессивно приостанавливает приложения для экономии памяти и времени автономной работы.
На этой странице подробно объясняется, как Tracelet взаимодействует со строгими политиками фонового выполнения Apple и как вы можете настроить его для вашего конкретного сценария.
Разрешения и настройка Info.plist
Apple применяет строгие требования к конфиденциальности. Чтобы эффективно использовать Tracelet, вы должны точно указать, почему вам нужны определенные разрешения в вашем ios/Runner/Info.plist. Apple отклонит ваше приложение во время проверки в App Store, если эти строки отсутствуют или не объясняют четко вариант использования.
1. Требуемые описания использования
Добавьте следующие ключи в ваш Info.plist:
<!-- Required for basic tracking -->
<key>NSLocationWhenInUseUsageDescription</key>
<string>We need your location to track your route while the app is open.</string>
<!-- Required for background tracking when the app is minimized or killed -->
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>We need background location to record your route even when the app is closed.</string>
<!-- Required for the smart motion-detection battery saving engine -->
<key>NSMotionUsageDescription</key>
<string>Motion detection allows battery-efficient tracking by pausing GPS when stationary.</string>2. Фоновые режимы
Чтобы Tracelet работал в фоновом режиме (и выполнял автономный код Dart при закрытии приложения), вы должны объявить соответствующие возможности фоновых режимов в Xcode.
Альтернативно добавьте их непосредственно в Info.plist:
<key>UIBackgroundModes</key>
<array>
<string>location</string>
<string>fetch</string>
<!-- Only add 'audio' if you are using 'preventSuspend: true' (See Scenario 1 below) -->
</array>3. Удаление дополнительных функций (без сбоев)
Если вашему клиенту не нужны определенные функции (например, он не хочет отслеживать типы движения или активности), вы можете безопасно опустить соответствующий ключ в своем Info.plist. Собственный Swift-код Tracelet безопасно проверяет наличие ключа перед запросом функций.
| Ключ функции | Эффект при удалении из Info.plist |
|---|---|
| НОТРАНС0ЛАТЕ | Безопасно. iOS автоматически вернется к проверке изменений местоположения на наличие движения. CMMotionActivityManager не будет вызываться, и пользователю не будут запрашиваться разрешения на движение. |
UIBackgroundModes -> audio | Безопасно. Требуется только в том случае, если в вашей конфигурации установлен preventSuspend: true для предотвращения приостановки изоляции Dart. |
| НОТРАНС0ЛАТЕ | Безопасно. Приложение будет отслеживать только на переднем плане (или временно с помощью синей таблетки). Tracelet самостоятельно обрабатывает постепенное ухудшение. |
Сценарий 1. Фитнес-приложение (высокая точность, без приостановок)
Изученные понятия: Типы активности, приостановка, звуковое пробуждение.
Проблема
Ваш пользователь бежит марафон. Они открывают ваше приложение, начинают пробежку и блокируют свой iPhone. Пять минут спустя iOS решает, что ей нужна оперативная память для запуска фонового резервного копирования iCloud, поэтому она полностью приостанавливает изоляцию Dart вашего приложения. Собственный код Swift Tracelet продолжает собирать точки GPS, но поскольку изолят Dart заморожен, ваш пользовательский интерфейс Flutter перестает обновлять счетчик расстояния, и все веб-сокеты отслеживания в реальном времени, которые вы используете в Dart, уничтожаются.
Как Tracelet решает эту проблему: предотвращение приостановки
Чтобы изоляция Flutter Dart работала бесконечно, пока экран выключен, вы должны обмануть iOS, заставив ее думать, что приложение активно воспроизводит мультимедиа.
ios: tl.IosConfig(
activityType: tl.LocationActivityType.fitness, // Optimizes GPS filtering for running
preventSuspend: true,
)Установка preventSuspend: true заставляет Tracelet воспроизводить незаметный, тихий аудиоклип в цикле. Поскольку iOS считает, что пользователь слушает музыку, она НИКОГДА не приостанавливает изоляцию Dart.
Примечание к обзору App Store: Для использования preventSuspend требуется возможность фонового режима audio в Xcode. Apple отклонит ваше приложение, если вы используете фоновый звук исключительно для поддержания работоспособности приложения без уважительной причины, связанной с пользователем (например, фитнес-трекер воспроизводит голосовые подсказки или навигационное приложение произносит пошаговые инструкции). Не используйте это для бесшумного отслеживания автопарка!
Чтобы включить это: Откройте проект iOS в Xcode, перейдите в раздел Подписание и возможности -> + Возможности -> Фоновые режимы -> установите флажок Аудио, AirPlay и «Картинка в картинке».
Сценарий 2: Социальный радар (малая мощность, грубый анализ)
Изученные концепции: Значительные изменения, уровни авторизации
Проблема
Вы создаете приложение для социальной сети, которое уведомляет пользователя, когда друг находится поблизости. Вам не нужна точность от поворота к повороту; вам просто нужно примерно знать, в каком районе они находятся. Вы также не хотите показывать пугающую подсказку о разрешении «Всегда разрешать», поскольку пользователи отклонят ее.
Как Tracelet решает эту проблему: значительные изменения
Вместо включения питания GPS-чипа Tracelet может полностью полагаться на передачу обслуживания вышки сотовой связи.
ios: tl.IosConfig(
useSignificantChangesOnly: true,
locationAuthorizationRequest: tl.LocationAuthorizationRequest.whenInUse,
disableLocationAuthorizationAlert: true,
)- Только существенные изменения. Включив этот параметр, Tracelet сообщает iOS, что приложение будет активироваться только тогда, когда устройство переходит к совершенно другой вышке сотовой связи (обычно от 500 м до нескольких километров). Расход батареи практически нулевой.
- Разрешение при использовании. Вы запрашиваете только разрешение «При использовании». Tracelet учитывает это и не запускает встроенное приглашение Apple, предлагающее пользователю перейти в настройки и включить «Всегда». (Чтобы узнать, как активировать эти запросы разрешений из Dart, см. страницу Flutter SDK: Разрешения).
Сценарий 3: Синий индикатор местоположения (showsBackgroundLocationIndicator)
Изученные концепции: Индикатор местоположения фона
Что на самом деле делает этот флаг
showsBackgroundLocationIndicator напрямую соответствует Apple
CLLocationManager.showsBackgroundLocationIndicator. Название нелогично — это отображение индикатора, а не переключатель, позволяющий его скрыть*:
| Значение | Значение |
|---|---|
| НОТРАНС0ЛАТЕ | Показывать синюю таблетку в строке состояния/индикатор динамического острова, когда приложение использует местоположение в фоновом режиме. |
false (по умолчанию) | Запрос на скрытие индикатора фонового использования местоположения. |
Распространенное заблуждение: установка showsBackgroundLocationIndicator: true не выключает синий индикатор, а включает его. Если ваша цель — скрыть его, оставьте false (по умолчанию). Установка true полезна только тогда, когда вы хотите, чтобы таблетка была видимой (например, чтобы обеспечить фоновое отслеживание «При использовании», ниже).
Когда вы хочу принять таблетку (временное отслеживание фона в разделе «При использовании»)
У вас есть только разрешение «При использовании», но вы хотите отслеживать, пока пользователь выполняет задачу (например, попутку). iOS разрешает это только если индикатор виден, чтобы пользователь знал, что происходит отслеживание:
ios: tl.IosConfig(
showsBackgroundLocationIndicator: true,
)Чтобы это включить: в Xcode должен быть включен фоновый режим location (Подписание и возможности → Фоновые режимы → Обновления местоположения).
Почему false не всегда это скрывает
Даже при использовании showsBackgroundLocationIndicator: false iOS принудительно включает индикатор в этих случаях — флаг не может их переопределить:
- Авторизация «При использовании» — фоновое местоположение всегда показывает индикатор. Только полная авторизация “Всегда” может подавить его.
useBackgroundActivitySession: true(iOS 17+) — Apple требует индикатор, покаCLBackgroundActivitySessionактивен (сценарий 4).- Активность активна (Сценарий 5).
- Любой непрерывный фоновый сеанс определения местоположения — если приложение работает
startUpdatingLocationнепрерывно, индикатор горит независимо от флага.
Чтобы на самом деле сохранить индикатор скрытым
- Получите полную авторизацию «Всегда» (обратите внимание, что iOS может сначала предоставить предварительного разрешение «При использовании», пока пользователь позже не подтвердит «Изменить на Всегда» — до этого времени появится табличка).
- Оставьте
showsBackgroundLocationIndicator: false(не устанавливайтеtrue). - Избегайте ненужного непрерывного фонового расположения. В режиме «Только геозона» используется собственный мониторинг региона (без непрерывного GPS), поэтому индикатор не отображается —
startGeofences()не выполняет непрерывные обновления в стандартном режиме.
Сценарий 4: Индикатор Dynamic Island (iOS 17+)
Изученные концепции: Фоновое занятие
Проблема
Вам нужны преимущества синего индикатора таблетки, но вам нужна более современная, встроенная интеграция с Dynamic Island на современных iPhone, и вы хотите снизить вероятность того, что ОС завершит ваш фоновый сеанс.
Как Tracelet решает эту проблему
Tracelet интегрируется с CLBackgroundActivitySession от Apple (представленным в iOS 17). Это обеспечивает заметный индикатор Dynamic Island и создает формальный сеанс с ОС, сообщая ей не приостанавливать работу вашего приложения.
ios: tl.IosConfig(
useBackgroundActivitySession: true,
)Требования к проверке App Store: Apple требует четкого объяснения того, почему вашему приложению требуется постоянное фоновое местоположение. Если вы используете CLBackgroundActivitySession, вы должны предоставить обоснование в трех местах, иначе ваше приложение будет отклонено:
- Примечания к обзору App Store Connect. Вы должны предоставить рецензенту четкое письменное объяснение того, почему приложению нужна эта функция, а также ссылку на демонстрационное видео, показывающее эту функцию в действии.
- Описание в App Store. В вашем общедоступном описании приложения должно быть четко указано, что приложение использует фоновое местоположение (например, «Это приложение использует фоновое местоположение, чтобы отслеживать ваши пробежки, даже когда приложение закрыто»).
- Объявление в приложении. Прежде чем запрашивать разрешения на определение местоположения, пользовательский интерфейс вашего приложения должен четко объяснить пользователю, почему необходимо фоновое определение местоположения.
Сценарий 5. Живые действия (экран блокировки и динамический островной пользовательский интерфейс)
Изученные концепции: ActivityKit, виджеты экрана блокировки, Dynamic Island.
Проблема
При отслеживании в фоновом режиме на iOS 17+ вам нужен насыщенный, понятный индикатор на экране блокировки и динамическом острове, чтобы пользователь всегда знал, что отслеживание активно, а не просто маленькая синяя таблетка местоположения.
Как Tracelet решает эту проблему
Tracelet интегрируется с ActivityKit от Apple. Если вы предоставите liveActivityConfig и добавите расширение виджета, Tracelet автоматически запускает интерактивное действие при запуске отслеживания и завершает его, когда отслеживание прекращается.
Live Activity — это уровень пользовательского интерфейса поверх стандартного фонового конвейера Tracelet. Экономия заряда батареи обеспечивается за счет механизма обнаружения движения, приостанавливающего работу GPS в неподвижном состоянии, и интеграции фонового сеанса (сценарий 4), а не за счет виджета активности. Tracelet не открывает второй поток CLLocationUpdate.liveUpdates(), который дублирует работу GPS.
ios: tl.IosConfig(
liveActivityConfig: tl.LiveActivityConfig(
title: 'Ride in progress',
body: 'Tracking your route to the destination...',
),
)Обновление активности в режиме реального времени во время отслеживания (updateNotification())
Чтобы изменить то, что показывает интерактивная активность после начала отслеживания, обновите liveActivityConfig через setConfig(), а затем вызовите Tracelet.updateNotification(). Начиная с версии v3.6.8, это обновляет Live Activity из последней конфигурации без перезапуска конвейера отслеживания — кросс-платформенный аналог обновления уведомления службы переднего плана Android:
await Tracelet.setConfig(
const tl.Config(
ios: tl.IosConfig(
liveActivityConfig: tl.LiveActivityConfig(
title: 'Ride in progress',
body: 'Arriving in 2 minutes',
),
),
),
);
await Tracelet.updateNotification();При выполняемом действии обновляется только body — title находится в неизменяемом ActivityAttributes и не может измениться без завершения и повторного запроса действия (ограничение ActivityKit).
Живое действие привязано к движущемуся подсостоянию (оно закрывается, когда Tracelet приостанавливает работу GPS при остановке), поэтому его может не быть на экране в момент его обновления. Пока сеанс отслеживания активен, updateNotification() делает это за вас: обновляет текущую активность или повторно представляет ее с новейшим содержимым, если она была отклонена. Это безопасный режим бездействия, если liveActivityConfig не установлен или отслеживание остановлено.
Настройка расширения виджета Xcode (обязательно)
Для чего это? Эта настройка позволяет вашему приложению отображать живую активность на экране блокировки iOS и динамическом острове, пока активно отслеживание. Он предоставляет пользователям наглядную информацию о текущем сеансе, не открывая приложение.
Экономит ли это заряд батареи? Нет. Live Activity — это всего лишь уровень пользовательского интерфейса, который не экономит заряд батареи. Эффективность использования батареи полностью зависит от основного фонового механизма Tracelet (обнаружение движения, приостановка GPS в неподвижном состоянии и фоновые сеансы).
Обязательно ли это? Нет. Эта настройка не является обязательной. Если вы пропустите это, Tracelet по-прежнему будет отлично отслеживать в фоновом режиме, но пользователи будут видеть только стандартные системные индикаторы (например, синюю табличку местоположения) вместо вашего пользовательского интерфейса.
В отличие от Android, плагины Flutter не могут динамически создавать виджеты iOS. Чтобы включить Live Activity, вы должны добавить цель Widget Extension в свое iOS-приложение в Xcode:
- Откройте
ios/Runner.xcworkspaceв Xcode. - Перейдите в Файл -> Создать -> Цель… и выберите Расширение виджета.
- Назовите его
TraceletWidget. Убедитесь, что установлен флажок Включить живую активность. - И в
Info.plistвашего приложения, и вInfo.plistвашего нового расширения виджета (щелкните их правой кнопкой мыши в Xcode -> Открыть как -> Исходный код), вы необходимы добавить следующий ключ внутри основного<dict>:
<key>NSSupportsLiveActivities</key>
<true/>- Синхронизируйте версию расширения (имеет значение только для отправки в App Store): Apple отклоняет расширение приложения,
CFBundleShortVersionStringилиCFBundleVersionкоторого не соответствует хост-приложению. Настройте Версию и Сборку целевого виджета в соответствии с вашим приложением: выберите цель TraceletWidget → Общие → Идентификация и задайте для Версия версию вашего приложения, а для Сборка — номер сборки вашего приложения (те же значения, что и дляpubspec.yaml).
Не пытайтесь установить их, вставив $(FLUTTER_BUILD_NAME) / $(FLUTTER_BUILD_NUMBER) в Info.plist виджета. Эти переменные определены только для цели Runner (через Generated.xcconfig Flutter); в цели виджета они разрешаются как пустые, поэтому версии молча не совпадают. Используйте настройки сборки цели (MARKETING_VERSION/CURRENT_PROJECT_VERSION), как указано выше. Это только требование к отправке — несоответствие не приведет к сбою приложения во время выполнения.
- НЕ связывайте зависимости Flutter: Не связывайте
FlutterGeneratedPluginSwiftPackageили движок Flutter с вашим расширением виджета. Это приводит к сбоюdyldво время запуска (Library not loaded) в режиме выпуска, поскольку Flutter не встраивает свои динамические структуры SPM в расширения приложений. - Замените автоматически сгенерированный контент
TraceletWidgetLiveActivity.swiftследующим. Обратите внимание, что здесь мы вручную определяем структуруTraceletActivityAttributesвместо импорта SDK — ActivityKit сопоставляет активность по (неполному) имени и форме структуры, поэтому это позволяет сделать ваш виджет легким и избежать связывания SDK с расширением:
import ActivityKit
import WidgetKit
import SwiftUI
// Define the exact struct expected by Tracelet's native core
public struct TraceletActivityAttributes: ActivityAttributes {
public struct ContentState: Codable, Hashable {
public var status: String
public init(status: String) { self.status = status }
}
public var title: String
public init(title: String) { self.title = title }
}
@main
struct TraceletWidgetBundle: WidgetBundle {
var body: some Widget {
TraceletWidgetLiveActivity()
}
}
struct TraceletWidgetLiveActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: TraceletActivityAttributes.self) { context in
// Lock Screen / banner UI — fully self-contained (no SDK import needed)
HStack(spacing: 12) {
Image(systemName: "location.fill").foregroundColor(.blue)
VStack(alignment: .leading) {
Text(context.attributes.title).font(.headline)
Text(context.state.status).font(.subheadline).foregroundColor(.secondary)
}
Spacer()
}
.padding()
} dynamicIsland: { context in
DynamicIsland {
DynamicIslandExpandedRegion(.leading) {
Text("Tracking")
}
DynamicIslandExpandedRegion(.trailing) {
Text("Live")
}
DynamicIslandExpandedRegion(.bottom) {
Text(context.state.status)
}
} compactLeading: {
Image(systemName: "location.fill").foregroundColor(.blue)
} compactTrailing: {
Text("Live")
} minimal: {
Image(systemName: "location.fill").foregroundColor(.blue)
}
}
}
}Это представление SwiftUI полностью принадлежит вам: оформляйте его по своему усмотрению внутри блока ActivityConfiguration, сопоставляя данные из context.attributes.title и context.state.status.
Установите в качестве цели развертывания расширения виджета iOS 16.2. Шаблон «Расширение виджета» Xcode часто добавляет управляющие виджеты и аннотацию @available(iOS 18.0, *) к @main WidgetBundle. Если эта аннотация превышает целевое значение развертывания расширения, пакет виджетов (и, следовательно, ваша интерактивная активность) автоматически не зарегистрируется — система регистрирует Activity had no descriptor — и расширение может привести к сбою в старых версиях ОС. Держите пакет @main доступным на этапе развертывания расширения и оберните все виджеты, предназначенные только для iOS-18 (например, виджеты управления), в if #available(iOS 18.0, *).
Сценарий 6: Состояние завершения (принудительное завершение работы приложения)
Изученные концепции: Жизненный цикл приложения, принудительное завершение и приостановка работы ОС.
Проблема
Ваш пользователь проводит пальцем вверх от переключателя приложений и полностью принудительно закрывает приложение. Вы ожидаете, что Tracelet продолжит отслеживать их в фоновом режиме, но местоположения перестают поступать.
Как Apple с этим справляется
В отличие от Android, где служба переднего плана часто может пережить смахивание для закрытия, iOS строго соблюдает намерения пользователя.
Согласно Официальной документации Apple CoreLocation :
“Если пользователь принудительно закрывает ваше приложение, система не запускает его автоматически при поступлении новых событий местоположения. Пользователь должен явно перезапустить ваше приложение, прежде чем система возобновит доставку событий местоположения.”
Когда пользователь принудительно закрывает приложение вручную:
- Стандартное обновление местоположения: Полностью остановлено.
- Значительные изменения местоположения (SLC): полностью остановлено.
- Мониторинг региона (геозоны): полностью остановлен.
Будет ли это работать в состоянии прекращения действия?
- Если ОС завершила работу приложения (например, из-за нехватки памяти в фоновом режиме): Да. Apple автоматически перезапустит ваше приложение в фоновом режиме при возникновении события местоположения (например, значительного изменения или срабатывания геозоны). Собственное ядро Rust Tracelet автоматически активируется и справится с этим.
- Если пользователь явно принудительно закрывает приложение: Нет. Приложение останется неработающим до тех пор, пока пользователь вручную не коснется значка приложения, чтобы открыть его снова.
Как Tracelet решает эту проблему
Tracelet не может обойти фундаментальные ограничения операционной системы Apple. Однако Tracelet гарантирует, что:
- Все несинхронизированные местоположения безопасно сохраняются в базе данных Rust SQLite до закрытия приложения.
- Как только пользователь снова открывает приложение, Tracelet мгновенно возобновляет отслеживание и синхронизирует любые офлайн-данные, не пропуская ни секунды.