diff --git a/Shared/Widget/WidgetDataEncoder.swift b/Shared/Widget/WidgetDataEncoder.swift index 79fd87d80..b0077be69 100644 --- a/Shared/Widget/WidgetDataEncoder.swift +++ b/Shared/Widget/WidgetDataEncoder.swift @@ -13,17 +13,34 @@ import UIKit import RSCore import Articles -@available(iOS 14, *) -struct WidgetDataEncoder { + +public final class WidgetDataEncoder { - private static var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Application") + private let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Application") - static func encodeWidgetData(refreshTimeline: Bool = true) { + 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 dataURL = containerURL?.appendingPathComponent("widget-data.json") + + static let shared = WidgetDataEncoder() + private init () {} + + @available(iOS 14, *) + func encodeWidgetData(refreshTimeline: Bool = true) throws { os_log(.debug, log: log, "Starting encoding widget data.") + do { - // Unread Articles - let unreadArticles = try SmartFeedsController.shared.unreadFeed.fetchArticles().sorted(by: { $0.datePublished ?? .distantPast > $1.datePublished ?? .distantPast }) + let unreadArticles = Array(try SmartFeedsController.shared.unreadFeed.fetchArticles()).sortedByDate(.orderedDescending) + + let starredArticles = Array(try SmartFeedsController.shared.starredFeed.fetchArticles()).sortedByDate(.orderedDescending) + + let todayArticles = Array(try SmartFeedsController.shared.todayFeed.fetchUnreadArticles()).sortedByDate(.orderedDescending) + var unread = [LatestArticle]() + var today = [LatestArticle]() + var starred = [LatestArticle]() + for article in unreadArticles { let latestArticle = LatestArticle(id: article.sortableArticleID, feedTitle: article.sortableName, @@ -32,13 +49,9 @@ struct WidgetDataEncoder { feedIcon: article.iconImage()?.image.dataRepresentation(), pubDate: article.datePublished!.description) unread.append(latestArticle) - if unread.count == 7 { break } } - // Starred Articles - let starredArticles = try SmartFeedsController.shared.starredFeed.fetchArticles().sorted(by: { $0.datePublished ?? .distantPast > $1.datePublished ?? .distantPast }) - var starred = [LatestArticle]() for article in starredArticles { let latestArticle = LatestArticle(id: article.sortableArticleID, feedTitle: article.sortableName, @@ -50,9 +63,6 @@ struct WidgetDataEncoder { if starred.count == 7 { break } } - // Today Articles - let todayArticles = try SmartFeedsController.shared.todayFeed.fetchUnreadArticles().sorted(by: { $0.datePublished ?? .distantPast > $1.datePublished ?? .distantPast }) - var today = [LatestArticle]() for article in todayArticles { let latestArticle = LatestArticle(id: article.sortableArticleID, feedTitle: article.sortableName, @@ -72,26 +82,44 @@ struct WidgetDataEncoder { todayArticles:today, lastUpdateTime: Date()) - let encodedData = try JSONEncoder().encode(latestData) - os_log(.debug, log: log, "Finished encoding widget data.") - let appGroup = Bundle.main.object(forInfoDictionaryKey: "AppGroup") as! String - let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) - let dataURL = containerURL?.appendingPathComponent("widget-data.json") - if FileManager.default.fileExists(atPath: dataURL!.path) { - try FileManager.default.removeItem(at: dataURL!) - os_log(.debug, log: log, "Removed widget data from container.") - } - if FileManager.default.createFile(atPath: dataURL!.path, contents: encodedData, attributes: nil) { - os_log(.debug, log: log, "Wrote widget data to container.") - if refreshTimeline == true { - WidgetCenter.shared.reloadAllTimelines() + + 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.") + + if self.fileExists() { + try? FileManager.default.removeItem(at: self.dataURL!) + os_log(.debug, log: self.log, "Removed widget data from container.") + } + if FileManager.default.createFile(atPath: self.dataURL!.path, contents: encodedData, attributes: nil) { + os_log(.debug, log: self.log, "Wrote widget data to container.") + if refreshTimeline == true { + WidgetCenter.shared.reloadAllTimelines() + UIApplication.shared.endBackgroundTask(self.backgroundTaskID!) + self.backgroundTaskID = .invalid + } else { + UIApplication.shared.endBackgroundTask(self.backgroundTaskID!) + self.backgroundTaskID = .invalid + } + } else { + UIApplication.shared.endBackgroundTask(self.backgroundTaskID!) + self.backgroundTaskID = .invalid + } + } - } catch { - os_log(.error, "%@", error.localizedDescription) } } + private func fileExists() -> Bool { + FileManager.default.fileExists(atPath: dataURL!.path) + } } diff --git a/Technotes/Widgets.md b/Technotes/Widgets.md index 628461506..3744938de 100644 --- a/Technotes/Widgets.md +++ b/Technotes/Widgets.md @@ -9,11 +9,10 @@ There are _currently_ seven widgets available for iOS: ## Widget Data The widget does not have access to the parent app's database. To surface data to the widget, a small amount of article data is encoded to JSON (see `WidgetDataEncoder`) and saved to the AppGroup container. -Widget data is written at three points: +Widget data is written at two points: -1. After applicationDidFinishLaunching -2. As part of a background refresh -3. When the scene enters the background +1. As part of a background refresh +2. When the scene enters the background The widget timeline is refreshed—via `WidgetCenter.shared.reloadAllTimelines()`—after each of the above. diff --git a/iOS/AppDelegate.swift b/iOS/AppDelegate.swift index 7e4cbcb17..180536ae5 100644 --- a/iOS/AppDelegate.swift +++ b/iOS/AppDelegate.swift @@ -115,11 +115,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD syncTimer!.update() #endif - if #available(iOS 14, *) { - WidgetDataEncoder.encodeWidgetData(refreshTimeline: false) - } - - return true } @@ -382,7 +377,7 @@ private extension AppDelegate { AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log) { [unowned self] in if !AccountManager.shared.isSuspended { if #available(iOS 14, *) { - WidgetDataEncoder.encodeWidgetData() + try? WidgetDataEncoder.shared.encodeWidgetData() } self.suspendApplication() os_log("Account refresh operation completed.", log: self.log, type: .info) diff --git a/iOS/SceneDelegate.swift b/iOS/SceneDelegate.swift index 9039eec7c..a8e5401ae 100644 --- a/iOS/SceneDelegate.swift +++ b/iOS/SceneDelegate.swift @@ -66,7 +66,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { func sceneDidEnterBackground(_ scene: UIScene) { if #available(iOS 14, *) { - WidgetDataEncoder.encodeWidgetData() + try? WidgetDataEncoder.shared.encodeWidgetData() } ArticleStringFormatter.emptyCaches() appDelegate.prepareAccountsForBackground()