Widget Performance

This commit is contained in:
Stuart Breckenridge 2020-12-03 20:32:26 +08:00
parent 3335c39c55
commit 8498e723ce
No known key found for this signature in database
GPG Key ID: 1F11FD62007DC331
4 changed files with 61 additions and 39 deletions

View File

@ -13,17 +13,34 @@ import UIKit
import RSCore import RSCore
import Articles 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.") os_log(.debug, log: log, "Starting encoding widget data.")
do { do {
// Unread Articles let unreadArticles = Array(try SmartFeedsController.shared.unreadFeed.fetchArticles()).sortedByDate(.orderedDescending)
let unreadArticles = try SmartFeedsController.shared.unreadFeed.fetchArticles().sorted(by: { $0.datePublished ?? .distantPast > $1.datePublished ?? .distantPast })
let starredArticles = Array(try SmartFeedsController.shared.starredFeed.fetchArticles()).sortedByDate(.orderedDescending)
let todayArticles = Array(try SmartFeedsController.shared.todayFeed.fetchUnreadArticles()).sortedByDate(.orderedDescending)
var unread = [LatestArticle]() var unread = [LatestArticle]()
var today = [LatestArticle]()
var starred = [LatestArticle]()
for article in unreadArticles { for article in unreadArticles {
let latestArticle = LatestArticle(id: article.sortableArticleID, let latestArticle = LatestArticle(id: article.sortableArticleID,
feedTitle: article.sortableName, feedTitle: article.sortableName,
@ -32,13 +49,9 @@ struct WidgetDataEncoder {
feedIcon: article.iconImage()?.image.dataRepresentation(), feedIcon: article.iconImage()?.image.dataRepresentation(),
pubDate: article.datePublished!.description) pubDate: article.datePublished!.description)
unread.append(latestArticle) unread.append(latestArticle)
if unread.count == 7 { break } 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 { for article in starredArticles {
let latestArticle = LatestArticle(id: article.sortableArticleID, let latestArticle = LatestArticle(id: article.sortableArticleID,
feedTitle: article.sortableName, feedTitle: article.sortableName,
@ -50,9 +63,6 @@ struct WidgetDataEncoder {
if starred.count == 7 { break } 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 { for article in todayArticles {
let latestArticle = LatestArticle(id: article.sortableArticleID, let latestArticle = LatestArticle(id: article.sortableArticleID,
feedTitle: article.sortableName, feedTitle: article.sortableName,
@ -72,26 +82,44 @@ struct WidgetDataEncoder {
todayArticles:today, todayArticles:today,
lastUpdateTime: Date()) lastUpdateTime: Date())
let encodedData = try JSONEncoder().encode(latestData)
os_log(.debug, log: log, "Finished encoding widget data.") DispatchQueue.global().async { [weak self] in
let appGroup = Bundle.main.object(forInfoDictionaryKey: "AppGroup") as! String guard let self = self else { return }
let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup)
let dataURL = containerURL?.appendingPathComponent("widget-data.json") self.backgroundTaskID = UIApplication.shared.beginBackgroundTask (withName: "com.ranchero.NetNewsWire.Encode") {
if FileManager.default.fileExists(atPath: dataURL!.path) { UIApplication.shared.endBackgroundTask(self.backgroundTaskID!)
try FileManager.default.removeItem(at: dataURL!) self.backgroundTaskID = .invalid
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()
} }
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)
}
} }

View File

@ -9,11 +9,10 @@ There are _currently_ seven widgets available for iOS:
## Widget Data ## 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. 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 1. As part of a background refresh
2. As part of a background refresh 2. When the scene enters the background
3. When the scene enters the background
The widget timeline is refreshed—via `WidgetCenter.shared.reloadAllTimelines()`—after each of the above. The widget timeline is refreshed—via `WidgetCenter.shared.reloadAllTimelines()`—after each of the above.

View File

@ -115,11 +115,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
syncTimer!.update() syncTimer!.update()
#endif #endif
if #available(iOS 14, *) {
WidgetDataEncoder.encodeWidgetData(refreshTimeline: false)
}
return true return true
} }
@ -382,7 +377,7 @@ private extension AppDelegate {
AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log) { [unowned self] in AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log) { [unowned self] in
if !AccountManager.shared.isSuspended { if !AccountManager.shared.isSuspended {
if #available(iOS 14, *) { if #available(iOS 14, *) {
WidgetDataEncoder.encodeWidgetData() try? WidgetDataEncoder.shared.encodeWidgetData()
} }
self.suspendApplication() self.suspendApplication()
os_log("Account refresh operation completed.", log: self.log, type: .info) os_log("Account refresh operation completed.", log: self.log, type: .info)

View File

@ -66,7 +66,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
func sceneDidEnterBackground(_ scene: UIScene) { func sceneDidEnterBackground(_ scene: UIScene) {
if #available(iOS 14, *) { if #available(iOS 14, *) {
WidgetDataEncoder.encodeWidgetData() try? WidgetDataEncoder.shared.encodeWidgetData()
} }
ArticleStringFormatter.emptyCaches() ArticleStringFormatter.emptyCaches()
appDelegate.prepareAccountsForBackground() appDelegate.prepareAccountsForBackground()