Background widget refresh
This commit is contained in:
parent
1e95c7d153
commit
6cd2bf248d
|
@ -13,7 +13,7 @@ public actor FervorClient: Sendable {
|
||||||
private let session: URLSession
|
private let session: URLSession
|
||||||
public private(set) var accessToken: String?
|
public private(set) var accessToken: String?
|
||||||
|
|
||||||
private let decoder: JSONDecoder = {
|
public static let decoder: JSONDecoder = {
|
||||||
let d = JSONDecoder()
|
let d = JSONDecoder()
|
||||||
let withFractionalSeconds = ISO8601DateFormatter()
|
let withFractionalSeconds = ISO8601DateFormatter()
|
||||||
withFractionalSeconds.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
withFractionalSeconds.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||||||
|
@ -47,16 +47,22 @@ public actor FervorClient: Sendable {
|
||||||
return components.url!
|
return components.url!
|
||||||
}
|
}
|
||||||
|
|
||||||
private func performRequest<T: Decodable>(_ request: URLRequest) async throws -> T {
|
private func configureRequest(_ request: URLRequest) -> URLRequest {
|
||||||
var request = request
|
var request = request
|
||||||
if let accessToken = accessToken {
|
if let accessToken = accessToken,
|
||||||
|
request.value(forHTTPHeaderField: "Authorization") == nil {
|
||||||
request.addValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
|
request.addValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
|
||||||
}
|
}
|
||||||
let (data, response) = try await session.data(for: request, delegate: nil)
|
return request
|
||||||
|
}
|
||||||
|
|
||||||
|
private func performRequest<T: Decodable>(_ request: URLRequest) async throws -> T {
|
||||||
|
let request = configureRequest(request)
|
||||||
|
let (data, response) = try await session.data(for: request)
|
||||||
if (response as! HTTPURLResponse).statusCode == 404 {
|
if (response as! HTTPURLResponse).statusCode == 404 {
|
||||||
throw Error.notFound
|
throw Error.notFound
|
||||||
}
|
}
|
||||||
let decoded = try decoder.decode(T.self, from: data)
|
let decoded = try FervorClient.decoder.decode(T.self, from: data)
|
||||||
return decoded
|
return decoded
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,6 +114,12 @@ public actor FervorClient: Sendable {
|
||||||
return try await performRequest(request)
|
return try await performRequest(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func itemsRequest(limit: Int) -> URLRequest {
|
||||||
|
return configureRequest(URLRequest(url: buildURL(path: "/api/v1/items", queryItems: [
|
||||||
|
URLQueryItem(name: "limit", value: limit.formatted()),
|
||||||
|
])))
|
||||||
|
}
|
||||||
|
|
||||||
public func item(id: FervorID) async throws -> Item? {
|
public func item(id: FervorID) async throws -> Item? {
|
||||||
let request = URLRequest(url: buildURL(path: "/api/v1/items/\(id)"))
|
let request = URLRequest(url: buildURL(path: "/api/v1/items/\(id)"))
|
||||||
do {
|
do {
|
||||||
|
|
|
@ -17,7 +17,7 @@ public class PersistentContainer: NSPersistentContainer, @unchecked Sendable {
|
||||||
return NSManagedObjectModel(contentsOf: url)!
|
return NSManagedObjectModel(contentsOf: url)!
|
||||||
}()
|
}()
|
||||||
|
|
||||||
private(set) lazy var backgroundContext: NSManagedObjectContext = {
|
public private(set) lazy var backgroundContext: NSManagedObjectContext = {
|
||||||
let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
|
let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
|
||||||
// todo: should the background context really be parented to the view context, or should they both be direct children of the PSC?
|
// todo: should the background context really be parented to the view context, or should they both be direct children of the PSC?
|
||||||
context.parent = self.viewContext
|
context.parent = self.viewContext
|
||||||
|
@ -126,6 +126,29 @@ public class PersistentContainer: NSPersistentContainer, @unchecked Sendable {
|
||||||
try await self.saveViewContext()
|
try await self.saveViewContext()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func upsertItems(_ items: [Fervor.Item]) async throws {
|
||||||
|
try await backgroundContext.perform {
|
||||||
|
try self.doUpsertItems(items, setProgress: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func doUpsertItems(_ items: [Fervor.Item], setProgress: ((_ current: Int, _ total: Int) -> Void)?) throws {
|
||||||
|
let req = Item.fetchRequest()
|
||||||
|
req.predicate = NSPredicate(format: "id in %@", items.map(\.id))
|
||||||
|
let existing = try self.backgroundContext.fetch(req)
|
||||||
|
self.logger.debug("doUpsertItems: updating \(existing.count, privacy: .public) items, inserting \(items.count - existing.count, privacy: .public)")
|
||||||
|
// todo: this feels like it'll be slow when there are many items
|
||||||
|
for (index, item) in items.enumerated() {
|
||||||
|
setProgress?(index, items.count)
|
||||||
|
if let existing = existing.first(where: { $0.id == item.id }) {
|
||||||
|
existing.updateFromServer(item)
|
||||||
|
} else {
|
||||||
|
let mo = Item(context: self.backgroundContext)
|
||||||
|
mo.updateFromServer(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public func syncItems(_ syncUpdate: ItemsSyncUpdate, setProgress: @escaping (_ current: Int, _ total: Int) -> Void) async throws {
|
public func syncItems(_ syncUpdate: ItemsSyncUpdate, setProgress: @escaping (_ current: Int, _ total: Int) -> Void) async throws {
|
||||||
try await backgroundContext.perform {
|
try await backgroundContext.perform {
|
||||||
self.logger.debug("syncItems: deleting \(syncUpdate.delete.count, privacy: .public) items")
|
self.logger.debug("syncItems: deleting \(syncUpdate.delete.count, privacy: .public) items")
|
||||||
|
@ -141,20 +164,7 @@ public class PersistentContainer: NSPersistentContainer, @unchecked Sendable {
|
||||||
// todo: does the background/view contexts need to get saved then?
|
// todo: does the background/view contexts need to get saved then?
|
||||||
}
|
}
|
||||||
|
|
||||||
let req = Item.fetchRequest()
|
try self.doUpsertItems(syncUpdate.upsert, setProgress: setProgress)
|
||||||
req.predicate = NSPredicate(format: "id in %@", syncUpdate.upsert.map(\.id))
|
|
||||||
let existing = try self.backgroundContext.fetch(req)
|
|
||||||
self.logger.debug("syncItems: updating \(existing.count, privacy: .public) items, inserting \(syncUpdate.upsert.count - existing.count, privacy: .public)")
|
|
||||||
// todo: this feels like it'll be slow when there are many items
|
|
||||||
for (index, item) in syncUpdate.upsert.enumerated() {
|
|
||||||
setProgress(index, syncUpdate.upsert.count)
|
|
||||||
if let existing = existing.first(where: { $0.id == item.id }) {
|
|
||||||
existing.updateFromServer(item)
|
|
||||||
} else {
|
|
||||||
let mo = Item(context: self.backgroundContext)
|
|
||||||
mo.updateFromServer(item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.backgroundContext.hasChanges {
|
if self.backgroundContext.hasChanges {
|
||||||
try self.backgroundContext.save()
|
try self.backgroundContext.save()
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
D68B303D2792204B00E8B3FA /* read.js in Resources */ = {isa = PBXBuildFile; fileRef = D68B303C2792204B00E8B3FA /* read.js */; };
|
D68B303D2792204B00E8B3FA /* read.js in Resources */ = {isa = PBXBuildFile; fileRef = D68B303C2792204B00E8B3FA /* read.js */; };
|
||||||
D68B30402792729A00E8B3FA /* AppSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68B303F2792729A00E8B3FA /* AppSplitViewController.swift */; };
|
D68B30402792729A00E8B3FA /* AppSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68B303F2792729A00E8B3FA /* AppSplitViewController.swift */; };
|
||||||
D68B304227932ED500E8B3FA /* UserActivities.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68B304127932ED500E8B3FA /* UserActivities.swift */; };
|
D68B304227932ED500E8B3FA /* UserActivities.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68B304127932ED500E8B3FA /* UserActivities.swift */; };
|
||||||
|
D6A8E878286126DB007C6A2B /* BackgroundManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A8E877286126DB007C6A2B /* BackgroundManager.swift */; };
|
||||||
D6C687EC272CD27600874C10 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C687EB272CD27600874C10 /* AppDelegate.swift */; };
|
D6C687EC272CD27600874C10 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C687EB272CD27600874C10 /* AppDelegate.swift */; };
|
||||||
D6C687EE272CD27600874C10 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C687ED272CD27600874C10 /* SceneDelegate.swift */; };
|
D6C687EE272CD27600874C10 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C687ED272CD27600874C10 /* SceneDelegate.swift */; };
|
||||||
D6C687F8272CD27700874C10 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D6C687F7272CD27700874C10 /* Assets.xcassets */; };
|
D6C687F8272CD27700874C10 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D6C687F7272CD27700874C10 /* Assets.xcassets */; };
|
||||||
|
@ -126,6 +127,7 @@
|
||||||
D68B303E27923C0000E8B3FA /* Reader.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Reader.entitlements; sourceTree = "<group>"; };
|
D68B303E27923C0000E8B3FA /* Reader.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Reader.entitlements; sourceTree = "<group>"; };
|
||||||
D68B303F2792729A00E8B3FA /* AppSplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSplitViewController.swift; sourceTree = "<group>"; };
|
D68B303F2792729A00E8B3FA /* AppSplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSplitViewController.swift; sourceTree = "<group>"; };
|
||||||
D68B304127932ED500E8B3FA /* UserActivities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivities.swift; sourceTree = "<group>"; };
|
D68B304127932ED500E8B3FA /* UserActivities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivities.swift; sourceTree = "<group>"; };
|
||||||
|
D6A8E877286126DB007C6A2B /* BackgroundManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundManager.swift; sourceTree = "<group>"; };
|
||||||
D6AB5E9A285F6FE100157F2F /* Fervor */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Fervor; sourceTree = "<group>"; };
|
D6AB5E9A285F6FE100157F2F /* Fervor */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Fervor; sourceTree = "<group>"; };
|
||||||
D6AB5E9B285F706F00157F2F /* Persistence */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Persistence; sourceTree = "<group>"; };
|
D6AB5E9B285F706F00157F2F /* Persistence */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Persistence; sourceTree = "<group>"; };
|
||||||
D6C687E8272CD27600874C10 /* Reader.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Reader.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
D6C687E8272CD27600874C10 /* Reader.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Reader.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
@ -307,6 +309,7 @@
|
||||||
D68408EE2794808E00E327D2 /* Preferences.swift */,
|
D68408EE2794808E00E327D2 /* Preferences.swift */,
|
||||||
D608238C27DE729E00D7D5F9 /* ItemListType.swift */,
|
D608238C27DE729E00D7D5F9 /* ItemListType.swift */,
|
||||||
D6EEDE8A285FA7FD009F854E /* LocalData+Migration.swift */,
|
D6EEDE8A285FA7FD009F854E /* LocalData+Migration.swift */,
|
||||||
|
D6A8E877286126DB007C6A2B /* BackgroundManager.swift */,
|
||||||
D6D5FA25285FEA5B00BBF188 /* Widgets */,
|
D6D5FA25285FEA5B00BBF188 /* Widgets */,
|
||||||
D65B18AF2750468B004A9448 /* Screens */,
|
D65B18AF2750468B004A9448 /* Screens */,
|
||||||
D6C687F7272CD27700874C10 /* Assets.xcassets */,
|
D6C687F7272CD27700874C10 /* Assets.xcassets */,
|
||||||
|
@ -630,6 +633,7 @@
|
||||||
D6D5FA24285FE9DB00BBF188 /* WidgetHelper.swift in Sources */,
|
D6D5FA24285FE9DB00BBF188 /* WidgetHelper.swift in Sources */,
|
||||||
D6E24363278BA1410005E546 /* ItemCollectionViewCell.swift in Sources */,
|
D6E24363278BA1410005E546 /* ItemCollectionViewCell.swift in Sources */,
|
||||||
D6E2436E278BD8160005E546 /* ReadViewController.swift in Sources */,
|
D6E2436E278BD8160005E546 /* ReadViewController.swift in Sources */,
|
||||||
|
D6A8E878286126DB007C6A2B /* BackgroundManager.swift in Sources */,
|
||||||
D65B18C127505348004A9448 /* HomeViewController.swift in Sources */,
|
D65B18C127505348004A9448 /* HomeViewController.swift in Sources */,
|
||||||
D6C687EE272CD27600874C10 /* SceneDelegate.swift in Sources */,
|
D6C687EE272CD27600874C10 /* SceneDelegate.swift in Sources */,
|
||||||
);
|
);
|
||||||
|
|
|
@ -10,6 +10,7 @@ import WebKit
|
||||||
import OSLog
|
import OSLog
|
||||||
import Combine
|
import Combine
|
||||||
import Persistence
|
import Persistence
|
||||||
|
import BackgroundTasks
|
||||||
|
|
||||||
@main
|
@main
|
||||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
|
@ -49,9 +50,23 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(updateAppearance), name: .appearanceChanged, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(updateAppearance), name: .appearanceChanged, object: nil)
|
||||||
updateAppearance()
|
updateAppearance()
|
||||||
|
|
||||||
|
BGTaskScheduler.shared.register(forTaskWithIdentifier: BackgroundManager.refreshIdentifier, using: nil) { task in
|
||||||
|
Task {
|
||||||
|
BackgroundManager.shared.handleBackgroundRefresh(task: task as! BGAppRefreshTask)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
|
||||||
|
if identifier == BackgroundManager.refreshIdentifier {
|
||||||
|
BackgroundManager.shared.doRefresh()
|
||||||
|
} else {
|
||||||
|
fatalError("Unknown background url session identifier: \(identifier)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: UISceneSession Lifecycle
|
// MARK: UISceneSession Lifecycle
|
||||||
|
|
||||||
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
|
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
|
||||||
|
|
|
@ -0,0 +1,132 @@
|
||||||
|
//
|
||||||
|
// BackgroundManager.swift
|
||||||
|
// Reader
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 6/20/22.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import BackgroundTasks
|
||||||
|
import OSLog
|
||||||
|
import Persistence
|
||||||
|
import Fervor
|
||||||
|
|
||||||
|
private let logger = Logger(subsystem: "net.shadowfacts.Reader", category: "BackgroundManager")
|
||||||
|
|
||||||
|
class BackgroundManager: NSObject {
|
||||||
|
static let shared = BackgroundManager()
|
||||||
|
|
||||||
|
private override init() {}
|
||||||
|
|
||||||
|
static let refreshIdentifier = "net.shadowfacts.Reader.refresh"
|
||||||
|
|
||||||
|
private var refreshTask: BGAppRefreshTask?
|
||||||
|
private var completedRefreshRequests = 0
|
||||||
|
|
||||||
|
private var receivedData: [Int: Data] = [:]
|
||||||
|
|
||||||
|
func scheduleRefresh() {
|
||||||
|
// we schedule refreshes from sceneDidEnterBackground, but there may be multiple scenes, and we only want one refresh task
|
||||||
|
BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: BackgroundManager.refreshIdentifier)
|
||||||
|
|
||||||
|
logger.debug("Scheduling background refresh task")
|
||||||
|
|
||||||
|
let request = BGAppRefreshTaskRequest(identifier: BackgroundManager.refreshIdentifier)
|
||||||
|
request.earliestBeginDate = Date(timeIntervalSinceNow: 2 * 60 * 60)
|
||||||
|
// request.earliestBeginDate = Date(timeIntervalSinceNow: 60)
|
||||||
|
|
||||||
|
do {
|
||||||
|
try BGTaskScheduler.shared.submit(request)
|
||||||
|
} catch {
|
||||||
|
logger.error("Unable to schedule app refresh: \(String(describing: error), privacy: .public)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleBackgroundRefresh(task: BGAppRefreshTask) {
|
||||||
|
logger.debug("Handling background refresh task")
|
||||||
|
|
||||||
|
scheduleRefresh()
|
||||||
|
|
||||||
|
guard !LocalData.accounts.isEmpty else {
|
||||||
|
task.setTaskCompleted(success: true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.refreshTask = task
|
||||||
|
self.completedRefreshRequests = 0
|
||||||
|
|
||||||
|
doRefresh()
|
||||||
|
|
||||||
|
task.expirationHandler = {
|
||||||
|
task.setTaskCompleted(success: self.completedRefreshRequests == LocalData.accounts.count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func doRefresh() {
|
||||||
|
let config = URLSessionConfiguration.background(withIdentifier: BackgroundManager.refreshIdentifier)
|
||||||
|
config.sessionSendsLaunchEvents = true
|
||||||
|
let backgroundSession = URLSession(configuration: config, delegate: self, delegateQueue: nil)
|
||||||
|
|
||||||
|
Task {
|
||||||
|
for account in LocalData.accounts {
|
||||||
|
let fervorController = await FervorController(account: account, session: backgroundSession)
|
||||||
|
let itemsRequest = await fervorController.client.itemsRequest(limit: 32)
|
||||||
|
let task = backgroundSession.dataTask(with: itemsRequest)
|
||||||
|
task.taskDescription = account.id.base64EncodedString()
|
||||||
|
task.resume()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func receivedData(_ data: Data, for task: URLSessionDataTask) {
|
||||||
|
guard let taskDescription = task.taskDescription,
|
||||||
|
let id = Data(base64Encoded: taskDescription),
|
||||||
|
let account = LocalData.account(with: id) else {
|
||||||
|
logger.error("Could not find account to handle request")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Task { @MainActor in
|
||||||
|
defer {
|
||||||
|
if let refreshTask {
|
||||||
|
refreshTask.setTaskCompleted(success: completedRefreshRequests == LocalData.accounts.count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let items: [Fervor.Item]
|
||||||
|
do {
|
||||||
|
items = try FervorClient.decoder.decode([Fervor.Item].self, from: data)
|
||||||
|
} catch {
|
||||||
|
logger.error("Unable to decode items: \(String(describing: error), privacy: .public)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let fervorController = await FervorController(account: account)
|
||||||
|
do {
|
||||||
|
try await fervorController.persistentContainer.upsertItems(items)
|
||||||
|
logger.info("Upserted \(items.count) items during background refresh")
|
||||||
|
|
||||||
|
await WidgetHelper.updateWidgetData(fervorController: fervorController)
|
||||||
|
|
||||||
|
completedRefreshRequests += 1
|
||||||
|
} catch {
|
||||||
|
logger.error("Unable to upsert items: \(String(describing: error), privacy: .public)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension BackgroundManager: URLSessionDataDelegate {
|
||||||
|
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
|
||||||
|
if !receivedData.keys.contains(dataTask.taskIdentifier) {
|
||||||
|
receivedData[dataTask.taskIdentifier] = Data(capacity: Int(dataTask.countOfBytesExpectedToReceive))
|
||||||
|
}
|
||||||
|
receivedData[dataTask.taskIdentifier]!.append(data)
|
||||||
|
|
||||||
|
logger.debug("Received chunk of \(data.count) bytes")
|
||||||
|
|
||||||
|
if dataTask.countOfBytesReceived == dataTask.countOfBytesExpectedToReceive {
|
||||||
|
logger.info("Received all data for task, handling update")
|
||||||
|
receivedData(receivedData.removeValue(forKey: dataTask.taskIdentifier)!, for: dataTask)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -31,9 +31,9 @@ actor FervorController {
|
||||||
private var lastSyncState = SyncState.done
|
private var lastSyncState = SyncState.done
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
init(instanceURL: URL, account: LocalData.Account?) async {
|
init(instanceURL: URL, account: LocalData.Account?, session: URLSession = .shared) async {
|
||||||
self.instanceURL = instanceURL
|
self.instanceURL = instanceURL
|
||||||
self.client = FervorClient(instanceURL: instanceURL, accessToken: account?.token.accessToken)
|
self.client = FervorClient(instanceURL: instanceURL, accessToken: account?.token.accessToken, session: session)
|
||||||
self.account = account
|
self.account = account
|
||||||
self.clientID = account?.clientID
|
self.clientID = account?.clientID
|
||||||
self.clientSecret = account?.clientSecret
|
self.clientSecret = account?.clientSecret
|
||||||
|
@ -45,8 +45,8 @@ actor FervorController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
convenience init(account: LocalData.Account) async {
|
convenience init(account: LocalData.Account, session: URLSession = .shared) async {
|
||||||
await self.init(instanceURL: account.instanceURL, account: account)
|
await self.init(instanceURL: account.instanceURL, account: account, session: session)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setSyncState(_ state: SyncState) {
|
private func setSyncState(_ state: SyncState) {
|
||||||
|
|
|
@ -37,5 +37,13 @@
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
||||||
|
<array>
|
||||||
|
<string>net.shadowfacts.Reader.refresh</string>
|
||||||
|
</array>
|
||||||
|
<key>UIBackgroundModes</key>
|
||||||
|
<array>
|
||||||
|
<string>fetch</string>
|
||||||
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|
|
@ -47,7 +47,9 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
fervorController = await FervorController(account: account)
|
fervorController = await FervorController(account: account)
|
||||||
syncFromServer()
|
syncFromServer()
|
||||||
createAppUI()
|
createAppUI()
|
||||||
if let activity = activity {
|
if !connectionOptions.urlContexts.isEmpty {
|
||||||
|
self.scene(scene, openURLContexts: connectionOptions.urlContexts)
|
||||||
|
} else if let activity = activity {
|
||||||
await setupUI(from: activity)
|
await setupUI(from: activity)
|
||||||
}
|
}
|
||||||
setupSceneActivationConditions()
|
setupSceneActivationConditions()
|
||||||
|
@ -90,15 +92,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
func sceneWillResignActive(_ scene: UIScene) {
|
func sceneWillResignActive(_ scene: UIScene) {
|
||||||
// Called when the scene will move from an active state to an inactive state.
|
// Called when the scene will move from an active state to an inactive state.
|
||||||
// This may occur due to temporary interruptions (ex. an incoming phone call).
|
// This may occur due to temporary interruptions (ex. an incoming phone call).
|
||||||
|
|
||||||
if let fervorController = fervorController {
|
|
||||||
Task(priority: .userInitiated) {
|
|
||||||
await fervorController.syncReadToServer()
|
|
||||||
}
|
|
||||||
Task(priority: .userInitiated) {
|
|
||||||
await WidgetHelper.updateWidgetData(fervorController: fervorController)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func sceneWillEnterForeground(_ scene: UIScene) {
|
func sceneWillEnterForeground(_ scene: UIScene) {
|
||||||
|
@ -110,6 +103,17 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
// Called as the scene transitions from the foreground to the background.
|
// Called as the scene transitions from the foreground to the background.
|
||||||
// Use this method to save data, release shared resources, and store enough scene-specific state information
|
// Use this method to save data, release shared resources, and store enough scene-specific state information
|
||||||
// to restore the scene back to its current state.
|
// to restore the scene back to its current state.
|
||||||
|
|
||||||
|
if let fervorController = fervorController {
|
||||||
|
Task(priority: .userInitiated) {
|
||||||
|
await fervorController.syncReadToServer()
|
||||||
|
}
|
||||||
|
Task(priority: .userInitiated) {
|
||||||
|
await WidgetHelper.updateWidgetData(fervorController: fervorController)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BackgroundManager.shared.scheduleRefresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? {
|
func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? {
|
||||||
|
|
|
@ -8,6 +8,9 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import WidgetKit
|
import WidgetKit
|
||||||
import Persistence
|
import Persistence
|
||||||
|
import OSLog
|
||||||
|
|
||||||
|
private let logger = Logger(subsystem: "net.shadowfacts.Reader", category: "WidgetHelper")
|
||||||
|
|
||||||
struct WidgetHelper {
|
struct WidgetHelper {
|
||||||
private init() {}
|
private init() {}
|
||||||
|
@ -17,12 +20,13 @@ struct WidgetHelper {
|
||||||
static func updateWidgetData(fervorController: FervorController) async {
|
static func updateWidgetData(fervorController: FervorController) async {
|
||||||
// Accessing CoreData from the widget extension puts us over the memory limit, so we pre-generate all the data it needs and save it to disk
|
// Accessing CoreData from the widget extension puts us over the memory limit, so we pre-generate all the data it needs and save it to disk
|
||||||
|
|
||||||
let prioritizedItems: [WidgetData.Item] = await fervorController.persistentContainer.performBackgroundTask { ctx in
|
let context = fervorController.persistentContainer.backgroundContext
|
||||||
|
let prioritizedItems: [WidgetData.Item] = await context.perform {
|
||||||
let req = Item.fetchRequest()
|
let req = Item.fetchRequest()
|
||||||
req.sortDescriptors = [NSSortDescriptor(key: "published", ascending: false)]
|
req.sortDescriptors = [NSSortDescriptor(key: "published", ascending: false)]
|
||||||
req.fetchLimit = 32
|
req.fetchLimit = 32
|
||||||
req.predicate = NSPredicate(format: "read = NO")
|
req.predicate = NSPredicate(format: "read = NO")
|
||||||
var items = (try? ctx.fetch(req)) ?? []
|
var items = (try? context.fetch(req)) ?? []
|
||||||
|
|
||||||
var prioritizedItems: [Item] = []
|
var prioritizedItems: [Item] = []
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue