mirror of
https://github.com/Ranchero-Software/NetNewsWire
synced 2025-08-12 06:26:36 +00:00
Create and use BackgroundTaskManager.
This commit is contained in:
169
iOS/BackgroundTaskManager.swift
Normal file
169
iOS/BackgroundTaskManager.swift
Normal file
@@ -0,0 +1,169 @@
|
||||
//
|
||||
// BackgroundTaskManager.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Brent Simmons on 2/1/25.
|
||||
// Copyright © 2025 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import BackgroundTasks
|
||||
import os
|
||||
import Account
|
||||
|
||||
protocol BackgroundTaskManagerDelegate: AnyObject {
|
||||
/// Called when application should suspend networking, database, and other processing.
|
||||
func backgroundTaskManagerApplicationShouldSuspend(_: BackgroundTaskManager)
|
||||
}
|
||||
|
||||
/// Registers and runs background tasks using the iOS BackgroundTasks API.
|
||||
final class BackgroundTaskManager {
|
||||
|
||||
static let shared = BackgroundTaskManager()
|
||||
|
||||
weak var delegate: BackgroundTaskManagerDelegate?
|
||||
|
||||
private var backgroundTaskDispatchQueue = DispatchQueue.init(label: "BGTaskScheduler")
|
||||
|
||||
private var waitBackgroundUpdateTask = UIBackgroundTaskIdentifier.invalid
|
||||
private var isWaitingForSyncTasks = false
|
||||
|
||||
private var syncBackgroundUpdateTask = UIBackgroundTaskIdentifier.invalid
|
||||
private var isSyncArticleStatusRunning = false
|
||||
|
||||
static let refreshTaskIdentifier = "com.ranchero.NetNewsWire.FeedRefresh"
|
||||
|
||||
private static let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "BackgroundTasks")
|
||||
|
||||
/// Register background feed refresh.
|
||||
func registerTasks() {
|
||||
BGTaskScheduler.shared.register(forTaskWithIdentifier: Self.refreshTaskIdentifier, using: nil) { task in
|
||||
self.performBackgroundFeedRefresh(with: task as! BGAppRefreshTask)
|
||||
}
|
||||
}
|
||||
|
||||
/// Schedules a background app refresh based on `AppDefaults.refreshInterval`.
|
||||
func scheduleBackgroundFeedRefresh() {
|
||||
let request = BGAppRefreshTaskRequest(identifier: Self.refreshTaskIdentifier)
|
||||
request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
|
||||
|
||||
// We send this to a dedicated serial queue because as of 11/05/19 on iOS 13.2 the call to the
|
||||
// task scheduler can hang indefinitely.
|
||||
backgroundTaskDispatchQueue.async {
|
||||
do {
|
||||
try BGTaskScheduler.shared.submit(request)
|
||||
} catch {
|
||||
Self.logger.error("Could not schedule app refresh: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func waitForSyncTasksToFinish() {
|
||||
guard !isWaitingForSyncTasks && UIApplication.shared.applicationState == .background else { return }
|
||||
|
||||
isWaitingForSyncTasks = true
|
||||
|
||||
waitBackgroundUpdateTask = UIApplication.shared.beginBackgroundTask {
|
||||
self.completeProcessing(true)
|
||||
Self.logger.info("Accounts wait for progress terminated for running too long.")
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.waitToComplete { suspend in
|
||||
self.completeProcessing(suspend)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func syncArticleStatus() {
|
||||
guard !isSyncArticleStatusRunning else { return }
|
||||
|
||||
isSyncArticleStatusRunning = true
|
||||
|
||||
let completeProcessing = { [unowned self] in
|
||||
self.isSyncArticleStatusRunning = false
|
||||
UIApplication.shared.endBackgroundTask(self.syncBackgroundUpdateTask)
|
||||
self.syncBackgroundUpdateTask = UIBackgroundTaskIdentifier.invalid
|
||||
}
|
||||
|
||||
self.syncBackgroundUpdateTask = UIApplication.shared.beginBackgroundTask {
|
||||
completeProcessing()
|
||||
Self.logger.info("Accounts sync processing terminated for running too long.")
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
AccountManager.shared.syncArticleStatusAll {
|
||||
completeProcessing()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension BackgroundTaskManager {
|
||||
|
||||
/// Performs background feed refresh.
|
||||
/// - Parameter task: `BGAppRefreshTask`
|
||||
/// - Warning: As of Xcode 11 beta 2, when triggered from the debugger this doesn't work.
|
||||
func performBackgroundFeedRefresh(with task: BGAppRefreshTask) {
|
||||
|
||||
scheduleBackgroundFeedRefresh() // schedule next refresh
|
||||
|
||||
Self.logger.info("Woken to perform account refresh.")
|
||||
|
||||
DispatchQueue.main.async {
|
||||
if AccountManager.shared.isSuspended {
|
||||
AccountManager.shared.resumeAll()
|
||||
}
|
||||
AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log) {
|
||||
if !AccountManager.shared.isSuspended {
|
||||
self.suspendApplication()
|
||||
Self.logger.info("Account refresh operation completed.")
|
||||
task.setTaskCompleted(success: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// set expiration handler
|
||||
task.expirationHandler = { [weak task] in
|
||||
Self.logger.info("Accounts refresh processing terminated for running too long.")
|
||||
DispatchQueue.main.async {
|
||||
self.suspendApplication()
|
||||
task?.setTaskCompleted(success: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func waitToComplete(completion: @escaping (Bool) -> Void) {
|
||||
guard UIApplication.shared.applicationState == .background else {
|
||||
Self.logger.info("App came back to foreground, no longer waiting.")
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
|
||||
if AccountManager.shared.refreshInProgress || isSyncArticleStatusRunning || WidgetDataEncoder.shared.isRunning {
|
||||
Self.logger.info("Waiting for sync to finish…")
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
|
||||
self.waitToComplete(completion: completion)
|
||||
}
|
||||
} else {
|
||||
Self.logger.info("Refresh progress complete.")
|
||||
completion(true)
|
||||
}
|
||||
}
|
||||
|
||||
func completeProcessing(_ suspend: Bool) {
|
||||
if suspend {
|
||||
suspendApplication()
|
||||
}
|
||||
UIApplication.shared.endBackgroundTask(self.waitBackgroundUpdateTask)
|
||||
waitBackgroundUpdateTask = UIBackgroundTaskIdentifier.invalid
|
||||
isWaitingForSyncTasks = false
|
||||
}
|
||||
|
||||
func suspendApplication() {
|
||||
assert(delegate != nil)
|
||||
assert(Thread.isMainThread)
|
||||
delegate?.backgroundTaskManagerApplicationShouldSuspend(self)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user