Долгосрочные фоновые задачи на iOS 16+

Вопрос или проблема

Я разработал простое приложение, чтобы продемонстрировать проблему, с которой я сталкиваюсь в моем основном проекте. Концепция проста: есть два экрана с двумя таймерами, каждый настроен на 10 секунд. Если iPhone обнаруживает движение во время первого таймера, он должен сброситься. Если движение обнаруживается во время второго таймера, приложение должно перейти к экрану с первым таймером и сбросить его также. Я также добавил уведомление, когда появляется второй таймер, и простой звуковой сигнал, когда обнаруживается движение, чтобы убедиться, что оно работает должным образом в фоновом режиме. Важно отметить, что приложение должно работать непрерывно в течение 10-12 часов даже при заблокированном iPhone. Однако оно не функционирует в фоновом режиме. Оно работает идеально в активном режиме или когда iPhone подключен к Xcode (режим разработчика), но не работает в фоновом режиме. Я также обновил info.plist и активировал Background Modes.

    import SwiftUI
import CoreMotion

struct FirstTimerView: View {
    let timerDuration: Int
    @State private var timeRemaining: Int
    var onTimerEnd: () -> Void
    var onCancel: () -> Void

    @StateObject private var motionManager = MotionManager()
    @State private var timer: Timer?

    init(timerDuration: Int, onTimerEnd: @escaping () -> Void, onCancel: @escaping () -> Void) {
        self.timerDuration = timerDuration
        self.onTimerEnd = onTimerEnd
        self.onCancel = onCancel
        _timeRemaining = State(initialValue: timerDuration)
    }

    var body: some View {
        VStack {
            Text("Первый таймер")
                .font(.largeTitle)
                .padding()
            Text("\(timeRemaining) секунд осталось")
                .font(.title)
                .padding()

            Button(action: {
                timer?.invalidate()
                motionManager.stopMotionDetection()
                onCancel()
            }) {
                Text("Отмена")
                    .padding()
                    .background(Color.red)
                    .foregroundColor(.white)
                    .cornerRadius(10)
            }
        }
        .onAppear {
            startTimer()
            motionManager.startMotionDetection()
        }
        .onChange(of: motionManager.hasMoved) { moved in
            if moved {
                print("🚨 [Первый таймер] Движение обнаружено. Таймер перезапускается!")
                resetTimer()
            }
        }
    }

    func startTimer() {
        timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
            if timeRemaining > 0 {
                timeRemaining -= 1
                print("⏳ [Первый таймер]: \(timeRemaining) секунд осталось")
            } else {
                timer?.invalidate()
                motionManager.stopMotionDetection()
                print("✅ [Первый таймер] завершен.")
                onTimerEnd()
            }
        }
    }

    func resetTimer() {
        timeRemaining = timerDuration
        print("🔄 [Первый таймер] Таймер сброшен на \(timerDuration) секунд")
    }
}
import SwiftUI
import CoreMotion
import UserNotifications

struct SecondTimerView: View {
    let timerDuration: Int
    @State private var timeRemaining: Int
    var onTimerEnd: () -> Void
    var onCancel: () -> Void

    @StateObject private var motionManager = MotionManager()
    @State private var timer: Timer?

    init(timerDuration: Int, onTimerEnd: @escaping () -> Void, onCancel: @escaping () -> Void) {
        self.timerDuration = timerDuration
        self.onTimerEnd = onTimerEnd
        self.onCancel = onCancel
        _timeRemaining = State(initialValue: timerDuration)
    }

    var body: some View {
        VStack {
            Text("Второй таймер")
                .font(.largeTitle)
                .padding()
            Text("\(timeRemaining) секунд осталось")
                .font(.title)
                .padding()

            Button(action: {
                timer?.invalidate()
                motionManager.stopMotionDetection()
                onCancel()
            }) {
                Text("Отмена")
                    .padding()
                    .background(Color.red)
                    .foregroundColor(.white)
                    .cornerRadius(10)
            }
        }
        .onAppear {
            sendFirstNotification()
            startTimer()
            motionManager.startMotionDetection()
        }
        .onChange(of: motionManager.hasMoved) { moved in
            if moved {
                print("🚨 [Второй таймер] Движение обнаружено. Возврат к первому таймеру.")
                timer?.invalidate()
                onCancel()  // Возврат к первому таймеру
            }
        }
    }

    func startTimer() {
        timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
            if timeRemaining > 0 {
                timeRemaining -= 1
                print("⏳ [Второй таймер]: \(timeRemaining) секунд осталось")
            } else {
                timer?.invalidate()
                motionManager.stopMotionDetection()
                print("✅ [Второй таймер] завершен.")
                sendNotification()  // Отправка уведомления, когда таймер завершен
                onTimerEnd()
            }
        }
    }

    func sendFirstNotification() {
        let content = UNMutableNotificationContent()
        content.title = "Второй таймер активирован"
        content.body = "Второй таймер активирован!"
        content.sound = UNNotificationSound.default

        // Срабатывание уведомления немедленно
        let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)

        // Создание запроса
        let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)

        // Планирование уведомления
        UNUserNotificationCenter.current().add(request) { error in
            if let error = error {
                print("Ошибка при планировании уведомления: \(error)")
            }
        }
    }

    // Функция для отправки локального уведомления, когда второй таймер заканчивается
    func sendNotification() {
        let content = UNMutableNotificationContent()
        content.title = "Второй таймер завершен"
        content.body = "Второй таймер завершен. Требуется действие!"
        content.sound = UNNotificationSound.default

        // Срабатывание уведомления немедленно
        let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)

        // Создание запроса
        let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)

        // Планирование уведомления
        UNUserNotificationCenter.current().add(request) { error in
            if let error = error {
                print("Ошибка при планировании уведомления: \(error)")
            }
        }
    }
}
import AVFoundation
import CoreLocation
import CoreMotion
import SwiftUI

class MotionManager: NSObject, ObservableObject, CLLocationManagerDelegate {
    private var motionManager = CMMotionManager()
    private var locationManager = CLLocationManager()
    private var audioPlayer: AVAudioPlayer?
    @Published var hasMoved: Bool = false

    override init() {
        super.init()
        locationManager.delegate = self
        locationManager.requestAlwaysAuthorization()
        locationManager.startUpdatingLocation()  // Поддерживает работу приложения в фоновом режиме

        motionManager.accelerometerUpdateInterval = 0.2
    }

    func startMotionDetection() {
        if motionManager.isAccelerometerAvailable {
            motionManager.startAccelerometerUpdates(to: OperationQueue.current!) { (data, error) in
                guard let accelerometerData = data else { return }
                let acceleration = accelerometerData.acceleration
                let totalAcceleration = sqrt(acceleration.x * acceleration.x + acceleration.y * acceleration.y + acceleration.z * acceleration.z)

                if totalAcceleration > 1.05 {  // Движение обнаружено
                    DispatchQueue.main.async {
                        self.hasMoved = true
                        self.playSound()  // Воспроизведение звука при обнаружении движения
                    }
                } else {
                    DispatchQueue.main.async {
                        self.hasMoved = false
                    }
                }
            }
        }
    }

    func stopMotionDetection() {
        motionManager.stopAccelerometerUpdates()
        locationManager.stopUpdatingLocation()
    }

    func playSound() {
        // Настройка аудиосессии для воспроизведения в фоне
        let audioSession = AVAudioSession.sharedInstance()
        do {
            try audioSession.setCategory(.playback, mode: .default, options: [])
            try audioSession.setActive(true)
        } catch {
            print("Не удалось установить категорию аудиосессии: \(error)")
        }

        if let soundURL = Bundle.main.url(forResource: "sound", withExtension: "mp3") {
            do {
                audioPlayer = try AVAudioPlayer(contentsOf: soundURL)
                audioPlayer?.play()
            } catch {
                print("Ошибка при воспроизведении звука: \(error)")
            }
        }
    }

    // Методы CLLocationManagerDelegate
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        // Обновления местоположения используются только для поддержания работы приложения в фоновом режиме
    }
}

Ответ или решение

Ваше описание проблемы с выполнением фоновых задач в iOS 16+ очень актуально и касается ряда ограничений, наложенных операционной системой. Рассмотрим несколько рекомендаций, как сделать вашу задачу работоспособной в фоновом режиме, и разберем несколько ключевых аспектов вашего кода.

1. Режимы фоновой работы

Сначала убедитесь, что вы правильно настроили info.plist. Для работы фоновых задач вам необходимо включить режимы, такие как:

  • Audio (для воспроизведения звуков)
  • Location updates (для получения данных о местоположении, что также помогает удерживать приложение активным в фоне)

Это можно сделать, добавив ключи (UIBackgroundModes) для audio и location в ваш info.plist.

2. Изменение логики работы с CMMotionManager

Ваш класс MotionManager использует CMMotionManager для отслеживания движения. Однако iOS может приостанавливать обновления, когда приложение находится в фоновом режиме. Чтобы улучшить надежность, вы можете рассмотреть использование Core Motion в сочетании с геолокационными обновлениями. Полная их комбинация может помочь в поддержании работы приложения в фоновом режиме.

3. Оптимизация использования Location Services

Если вы используете CLLocationManager для обеспечения фоновой работы, убедитесь, что вы:

  • Запрашиваете постоянный доступ к локации с помощью requestAlwaysAuthorization().
  • Настраиваете менеджер локации для обновления в фоновом режиме, даже если ваши другие фоновое состояние перерывами.

4. Уведомления и применение звуков

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

Ваша функция playSound() уже использует AVAudioSession, но важно, чтобы она также учитывала, что iOS может приостановить сессию аудио.

5. Решение проблемы с таймерами

Чтобы таймеры работали корректно в фоне, вы можете использовать DispatchSourceTimer, который более эффективен для фоновой работы. Вот пример, как это может выглядеть:

var backgroundTimer: DispatchSourceTimer?

func startBackgroundTimer() {
    backgroundTimer = DispatchSource.makeTimerSource()
    backgroundTimer?.schedule(deadline: .now(), repeating: 1.0)
    backgroundTimer?.setEventHandler { [weak self] in
        // Логика для снижения таймера
        DispatchQueue.main.async {
            self?.timeRemaining -= 1
            if self?.timeRemaining == 0 {
                self?.backgroundTimer?.cancel()
                self?.sendNotification() // или другое действие
            }
        }
    }
    backgroundTimer?.resume()
}

6. Рекомендации по развертыванию приложения

  1. Протестируйте приложение на реальном устройстве без подключения к Xcode, чтобы убедиться, что оно корректно работает в фоновом режиме.
  2. Обратите внимание на энергетические ограничения устройства: iOS может отключить вашу работу в фоновом режиме, если считает, что приложение излишне расходует ресурсы.

Заключение

Ваш проект требует несколько доработок для эффективной фоновой работы в пределах ограничений iOS. Основное внимание стоит уделить использованию Core Location наряду с CMMotionManager и оптимизации фоновых таймеров. Проводите тестирование на реальных устройствах и учитывайте потребление ресурсов, чтобы обеспечить непрерывную работу вашего приложения.

Оцените материал
Добавить комментарий

Капча загружается...