iOS13+后台任务问题求助:实现每15分钟检测新邮件推送通知
Hey there, I’ve wrestled with similar BGTaskScheduler quirks before, so let’s break down what’s going wrong and how to fix it. Your goal of checking for new emails every 15 minutes makes sense, but iOS’s background task system has some strict rules we need to follow.
First, Let’s Diagnose the Core Issues
Your code has a few missteps that are causing the "Can't end a bg task..." error and failed refreshes:
- Task completion timing: You’re calling
task.setTaskCompleted(success: true)immediately, before your email fetch logic finishes. BGTaskScheduler expects you to mark the task as complete only after your work is done (especially async operations like network calls). - Expiration handler logic: Canceling all pending tasks when the task expires is overkill—you should only clean up the current in-progress work instead.
- Missing Info.plist configuration: If you haven’t added your task identifier to
BGTaskSchedulerPermittedIdentifiers, iOS will ignore your task requests entirely. - Misunderstanding BGTask scheduling:
earliestBeginDatedoesn’t guarantee an exact 15-minute interval—iOS prioritizes battery life and system health, so tasks may be delayed or skipped.
Step 1: Fix Info.plist Configuration
First, add the required background task permissions:
- Open your
Info.plist - Add a new key
BGTaskSchedulerPermittedIdentifiers(array type) - Add your task identifier
com.InformationSystem.emailFetchas an item in the array
This is non-negotiable—without it, none of your background tasks will run.
Step 2: Revise Your Background Task Code
Let’s rewrite your relevant methods to follow iOS’s rules properly. I’ll add comments to explain each change:
Updated AppDelegate Code
import UIKit import BackgroundTasks import UserNotifications @main class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate { // Track the current email fetch network task to cancel it if the background task expires private var currentEmailFetchTask: URLSessionDataTask? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { UNUserNotificationCenter.current().delegate = self registerBackgroundTasks() return true } func applicationDidEnterBackground(_ application: UIApplication) { // Schedule the first background task when the app goes to background scheduleEmailFetcher() } func applicationDidBecomeActive(_ application: UIApplication) { // Cancel pending background tasks when the app is active (no need to run them in foreground) cancelAllPendingBGTasks() } // MARK: - Background Task Setup private func registerBackgroundTasks() { BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.InformationSystem.emailFetch", using: nil) { task in // Safely cast to BGProcessingTask (avoid force unwrapping!) guard let processingTask = task as? BGProcessingTask else { task.setTaskCompleted(success: false) return } self.handleEmailFetcher(task: processingTask) } } func cancelAllPendingBGTasks() { BGTaskScheduler.shared.cancelAllTaskRequests() // Also cancel any in-progress network task currentEmailFetchTask?.cancel() currentEmailFetchTask = nil } func handleEmailFetcher(task: BGProcessingTask) { // Set up expiration handler: clean up work if iOS kills the task early task.expirationHandler = { self.currentEmailFetchTask?.cancel() self.currentEmailFetchTask = nil task.setTaskCompleted(success: false) print("Background task expired before completing email fetch") } print("Starting background email fetch task...") // Replace with your actual email check API endpoint guard let fetchURL = URL(string: "https://your-api.com/check-for-new-emails") else { print("Invalid email fetch URL") task.setTaskCompleted(success: false) // Schedule the next task only after current one finishes self.scheduleEmailFetcher() return } // Perform async network request to check for new emails currentEmailFetchTask = URLSession.shared.dataTask(with: fetchURL) { [weak self] data, response, error in defer { // Clean up the network task reference when done self?.currentEmailFetchTask = nil } guard let self = self else { return } if let error = error { print("Email fetch failed with error: \(error.localizedDescription)") task.setTaskCompleted(success: false) } else { // Parse the response to check for new emails (implement this logic based on your API) let hasNewEmails = self.parseEmailResponse(data: data) if hasNewEmails { self.setNotification() print("New emails found—notification triggered") } else { print("No new emails") } task.setTaskCompleted(success: true) } // Schedule the next background task ONLY after the current one completes self.scheduleEmailFetcher() } currentEmailFetchTask?.resume() } func scheduleEmailFetcher() { let request = BGProcessingTaskRequest(identifier: "com.InformationSystem.emailFetch") request.requiresNetworkConnectivity = true request.requiresExternalPower = false // Set earliest start time—note: iOS may delay this based on system conditions request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60) do { try BGTaskScheduler.shared.submit(request) print("Background email fetch scheduled successfully") } catch { print("Failed to schedule email fetch: \(error.localizedDescription)") } } // MARK: - Helper Methods private func parseEmailResponse(data: Data?) -> Bool { // Replace with your actual parsing logic to detect new emails // Example: Parse JSON response and check for unread count > 0 guard let data = data else { return false } // Add your parsing code here... return true } private func setNotification() { // Your existing notification setup logic here let content = UNMutableNotificationContent() content.title = "New Email" content.body = "You have unread emails waiting for you!" content.sound = .default let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil) UNUserNotificationCenter.current().add(request) } }
Step 3: Fix the "Can't end a bg task..." Error
This error almost always happens when:
- You call
setTaskCompletedmultiple times - You mark the task as complete before your async work finishes
- The task has already expired when you try to complete it
The updated code fixes this by:
- Calling
setTaskCompletedonly once per task (either in the network completion handler or expiration handler) - Ensuring async work is properly tracked and canceled if the task expires
- Avoiding force unwrapping (which can lead to unexpected crashes that trigger false error messages)
Step 4: Test Correctly on Simulator
BGTaskScheduler doesn’t work reliably on Simulator with natural backgrounding. To test:
- Run your app in Xcode
- Send the app to background (press
Cmd + Shift + H) - In Xcode, go to Debug > Simulate Background Task
- Select your task identifier
com.InformationSystem.emailFetchto trigger it manually
Important Caveat About BGTaskScheduler
iOS does not guarantee exact 15-minute intervals for background tasks. System factors like battery level, network availability, and device activity will determine when your task runs. For more reliable new email notifications, consider using Apple Push Notification service (APNs)—have your server send a push notification when new emails arrive. This is far more efficient and reliable than polling with background tasks.
内容的提问来源于stack exchange,提问作者Peter Hlavatík

