diff --git a/Shared/Widget/WidgetDataEncoder.swift b/Shared/Widget/WidgetDataEncoder.swift index 3462892e4..5276470f0 100644 --- a/Shared/Widget/WidgetDataEncoder.swift +++ b/Shared/Widget/WidgetDataEncoder.swift @@ -20,21 +20,48 @@ public final class WidgetDataEncoder { private let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Application") private let fetchLimit = 7 - private var backgroundTaskID: UIBackgroundTaskIdentifier! private lazy var appGroup = Bundle.main.object(forInfoDictionaryKey: "AppGroup") as! String private lazy var containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) private lazy var imageContainer = containerURL?.appendingPathComponent("widgetImages", isDirectory: true) private lazy var dataURL = containerURL?.appendingPathComponent("widget-data.json") - static let shared = WidgetDataEncoder() - private init () { + private let encodeWidgetDataQueue = CoalescingQueue(name: "Encode the Widget Data", interval: 5.0) + + init () { if imageContainer != nil { try? FileManager.default.createDirectory(at: imageContainer!, withIntermediateDirectories: true, attributes: nil) } + if #available(iOS 14, *) { + NotificationCenter.default.addObserver(self, selector: #selector(statusesDidChange(_:)), name: .StatusesDidChange, object: nil) + } + } + + func encodeIfNecessary() { + encodeWidgetDataQueue.performCallsImmediately() + } + + @available(iOS 14, *) + @objc func statusesDidChange(_ note: Notification) { + encodeWidgetDataQueue.add(self, #selector(performEncodeWidgetData)) + } + + @available(iOS 14, *) + @objc private func performEncodeWidgetData() { + // We will be on the Main Thread when the encodeIfNecessary function is called. We want + // block the main thread in that case so that the widget data is encoded. If it is on + // a background Thread, it was called by the CoalescingQueue. In that case we need to + // move it to the Main Thread and want to execute it async. + if Thread.isMainThread { + encodeWidgetData() + } else { + DispatchQueue.main.async { + self.encodeWidgetData() + } + } } @available(iOS 14, *) - func encodeWidgetData() throws { + private func encodeWidgetData() { flushSharedContainer() os_log(.debug, log: log, "Starting encoding widget data.") @@ -89,10 +116,6 @@ public final class WidgetDataEncoder { DispatchQueue.global().async { [weak self] in guard let self = self else { return } - self.backgroundTaskID = UIApplication.shared.beginBackgroundTask (withName: "com.ranchero.NetNewsWire.Encode") { - UIApplication.shared.endBackgroundTask(self.backgroundTaskID!) - self.backgroundTaskID = .invalid - } let encodedData = try? JSONEncoder().encode(latestData) os_log(.debug, log: self.log, "Finished encoding widget data.") @@ -104,14 +127,11 @@ public final class WidgetDataEncoder { if FileManager.default.createFile(atPath: self.dataURL!.path, contents: encodedData, attributes: nil) { os_log(.debug, log: self.log, "Wrote widget data to container.") WidgetCenter.shared.reloadAllTimelines() - UIApplication.shared.endBackgroundTask(self.backgroundTaskID!) - self.backgroundTaskID = .invalid - } else { - UIApplication.shared.endBackgroundTask(self.backgroundTaskID!) - self.backgroundTaskID = .invalid } } + } catch { + os_log(.error, log: log, "WidgetDataEncoder failed to write the widget data.") } } diff --git a/iOS/AppDelegate.swift b/iOS/AppDelegate.swift index da4a43fee..8ba3c7288 100644 --- a/iOS/AppDelegate.swift +++ b/iOS/AppDelegate.swift @@ -45,6 +45,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD var webFeedIconDownloader: WebFeedIconDownloader! var extensionContainersFile: ExtensionContainersFile! var extensionFeedAddRequestFile: ExtensionFeedAddRequestFile! + var widgetDataEncoder: WidgetDataEncoder! var unreadCount = 0 { didSet { @@ -114,6 +115,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD extensionContainersFile = ExtensionContainersFile() extensionFeedAddRequestFile = ExtensionFeedAddRequestFile() + widgetDataEncoder = WidgetDataEncoder() + syncTimer = ArticleStatusSyncTimer() #if DEBUG @@ -172,6 +175,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD func prepareAccountsForBackground() { extensionFeedAddRequestFile.suspend() + widgetDataEncoder.encodeIfNecessary() syncTimer?.invalidate() scheduleBackgroundFeedRefresh() syncArticleStatus() @@ -398,9 +402,6 @@ private extension AppDelegate { } AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log) { [unowned self] in if !AccountManager.shared.isSuspended { - if #available(iOS 14, *) { - try? WidgetDataEncoder.shared.encodeWidgetData() - } self.suspendApplication() os_log("Account refresh operation completed.", log: self.log, type: .info) task.setTaskCompleted(success: true) @@ -445,9 +446,6 @@ private extension AppDelegate { self.prepareAccountsForBackground() account!.syncArticleStatus(completion: { [weak self] _ in if !AccountManager.shared.isSuspended { - if #available(iOS 14, *) { - try? WidgetDataEncoder.shared.encodeWidgetData() - } self?.prepareAccountsForBackground() self?.suspendApplication() } @@ -474,9 +472,6 @@ private extension AppDelegate { account!.markArticles(article!, statusKey: .starred, flag: true) { _ in } account!.syncArticleStatus(completion: { [weak self] _ in if !AccountManager.shared.isSuspended { - if #available(iOS 14, *) { - try? WidgetDataEncoder.shared.encodeWidgetData() - } self?.prepareAccountsForBackground() self?.suspendApplication() } diff --git a/iOS/SceneDelegate.swift b/iOS/SceneDelegate.swift index 523446eed..7f5d14d3f 100644 --- a/iOS/SceneDelegate.swift +++ b/iOS/SceneDelegate.swift @@ -66,9 +66,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } func sceneDidEnterBackground(_ scene: UIScene) { - if #available(iOS 14, *) { - try? WidgetDataEncoder.shared.encodeWidgetData() - } ArticleStringFormatter.emptyCaches() appDelegate.prepareAccountsForBackground() }