Merge pull request #2626 from stuartbreckenridge/widget-performance
Widget Performance
This commit is contained in:
commit
63d7e179e8
|
@ -13,17 +13,34 @@ import UIKit
|
||||||
import RSCore
|
import RSCore
|
||||||
import Articles
|
import Articles
|
||||||
|
|
||||||
@available(iOS 14, *)
|
|
||||||
struct WidgetDataEncoder {
|
|
||||||
|
|
||||||
private static var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Application")
|
public final class WidgetDataEncoder {
|
||||||
|
|
||||||
static func encodeWidgetData(refreshTimeline: Bool = true) {
|
private let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Application")
|
||||||
|
|
||||||
|
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() 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,39 @@ 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) {
|
let encodedData = try? JSONEncoder().encode(latestData)
|
||||||
os_log(.debug, log: log, "Wrote widget data to container.")
|
|
||||||
if refreshTimeline == true {
|
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.")
|
||||||
WidgetCenter.shared.reloadAllTimelines()
|
WidgetCenter.shared.reloadAllTimelines()
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Reference in New Issue