Compare commits
No commits in common. "307299dd4d17d6bc6691d2dbf8dcf27008194d37" and "ae0c91d7190abe04c3644003a219b7f2959fccee" have entirely different histories.
307299dd4d
...
ae0c91d719
|
@ -13,7 +13,7 @@ public actor FervorClient: Sendable {
|
|||
private let session: URLSession
|
||||
public private(set) var accessToken: String?
|
||||
|
||||
public static let decoder: JSONDecoder = {
|
||||
private let decoder: JSONDecoder = {
|
||||
let d = JSONDecoder()
|
||||
let withFractionalSeconds = ISO8601DateFormatter()
|
||||
withFractionalSeconds.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||||
|
@ -47,22 +47,16 @@ public actor FervorClient: Sendable {
|
|||
return components.url!
|
||||
}
|
||||
|
||||
private func configureRequest(_ request: URLRequest) -> URLRequest {
|
||||
private func performRequest<T: Decodable>(_ request: URLRequest) async throws -> T {
|
||||
var request = request
|
||||
if let accessToken = accessToken,
|
||||
request.value(forHTTPHeaderField: "Authorization") == nil {
|
||||
if let accessToken = accessToken {
|
||||
request.addValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
|
||||
}
|
||||
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)
|
||||
let (data, response) = try await session.data(for: request, delegate: nil)
|
||||
if (response as! HTTPURLResponse).statusCode == 404 {
|
||||
throw Error.notFound
|
||||
}
|
||||
let decoded = try FervorClient.decoder.decode(T.self, from: data)
|
||||
let decoded = try decoder.decode(T.self, from: data)
|
||||
return decoded
|
||||
}
|
||||
|
||||
|
@ -114,12 +108,6 @@ public actor FervorClient: Sendable {
|
|||
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? {
|
||||
let request = URLRequest(url: buildURL(path: "/api/v1/items/\(id)"))
|
||||
do {
|
||||
|
|
|
@ -17,7 +17,7 @@ public class PersistentContainer: NSPersistentContainer, @unchecked Sendable {
|
|||
return NSManagedObjectModel(contentsOf: url)!
|
||||
}()
|
||||
|
||||
public private(set) lazy var backgroundContext: NSManagedObjectContext = {
|
||||
private(set) lazy var backgroundContext: NSManagedObjectContext = {
|
||||
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?
|
||||
context.parent = self.viewContext
|
||||
|
@ -126,29 +126,6 @@ public class PersistentContainer: NSPersistentContainer, @unchecked Sendable {
|
|||
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 {
|
||||
try await backgroundContext.perform {
|
||||
self.logger.debug("syncItems: deleting \(syncUpdate.delete.count, privacy: .public) items")
|
||||
|
@ -164,7 +141,20 @@ public class PersistentContainer: NSPersistentContainer, @unchecked Sendable {
|
|||
// todo: does the background/view contexts need to get saved then?
|
||||
}
|
||||
|
||||
try self.doUpsertItems(syncUpdate.upsert, setProgress: setProgress)
|
||||
let req = Item.fetchRequest()
|
||||
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 {
|
||||
try self.backgroundContext.save()
|
||||
|
|
|
@ -20,7 +20,6 @@
|
|||
D68B303D2792204B00E8B3FA /* read.js in Resources */ = {isa = PBXBuildFile; fileRef = D68B303C2792204B00E8B3FA /* read.js */; };
|
||||
D68B30402792729A00E8B3FA /* AppSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68B303F2792729A00E8B3FA /* AppSplitViewController.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 */; };
|
||||
D6C687EE272CD27600874C10 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C687ED272CD27600874C10 /* SceneDelegate.swift */; };
|
||||
D6C687F8272CD27700874C10 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D6C687F7272CD27700874C10 /* Assets.xcassets */; };
|
||||
|
@ -127,7 +126,6 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
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; };
|
||||
|
@ -309,7 +307,6 @@
|
|||
D68408EE2794808E00E327D2 /* Preferences.swift */,
|
||||
D608238C27DE729E00D7D5F9 /* ItemListType.swift */,
|
||||
D6EEDE8A285FA7FD009F854E /* LocalData+Migration.swift */,
|
||||
D6A8E877286126DB007C6A2B /* BackgroundManager.swift */,
|
||||
D6D5FA25285FEA5B00BBF188 /* Widgets */,
|
||||
D65B18AF2750468B004A9448 /* Screens */,
|
||||
D6C687F7272CD27700874C10 /* Assets.xcassets */,
|
||||
|
@ -633,7 +630,6 @@
|
|||
D6D5FA24285FE9DB00BBF188 /* WidgetHelper.swift in Sources */,
|
||||
D6E24363278BA1410005E546 /* ItemCollectionViewCell.swift in Sources */,
|
||||
D6E2436E278BD8160005E546 /* ReadViewController.swift in Sources */,
|
||||
D6A8E878286126DB007C6A2B /* BackgroundManager.swift in Sources */,
|
||||
D65B18C127505348004A9448 /* HomeViewController.swift in Sources */,
|
||||
D6C687EE272CD27600874C10 /* SceneDelegate.swift in Sources */,
|
||||
);
|
||||
|
|
|
@ -10,7 +10,6 @@ import WebKit
|
|||
import OSLog
|
||||
import Combine
|
||||
import Persistence
|
||||
import BackgroundTasks
|
||||
|
||||
@main
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
@ -50,23 +49,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||
NotificationCenter.default.addObserver(self, selector: #selector(updateAppearance), name: .appearanceChanged, object: nil)
|
||||
updateAppearance()
|
||||
|
||||
BGTaskScheduler.shared.register(forTaskWithIdentifier: BackgroundManager.refreshIdentifier, using: nil) { task in
|
||||
Task {
|
||||
BackgroundManager.shared.handleBackgroundRefresh(task: task as! BGAppRefreshTask)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
|
||||
|
|
|
@ -1,132 +0,0 @@
|
|||
//
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,7 +16,7 @@ public struct ExcerptGenerator {
|
|||
|
||||
private static let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "ExcerptGenerator")
|
||||
|
||||
static func generateAll(_ fervorController: FervorController, setProgress: @escaping (_ current: Int, _ total: Int) -> Void) async {
|
||||
static func generateAll(_ fervorController: FervorController) async {
|
||||
let req = Item.fetchRequest()
|
||||
req.predicate = NSPredicate(format: "generatedExcerpt = NO")
|
||||
req.sortDescriptors = [NSSortDescriptor(key: "published", ascending: false)]
|
||||
|
@ -31,7 +31,6 @@ public struct ExcerptGenerator {
|
|||
count += 1
|
||||
if count % 50 == 0 {
|
||||
logger.debug("Generated \(count, privacy: .public) excerpts")
|
||||
setProgress(count, items.count)
|
||||
}
|
||||
}
|
||||
item.generatedExcerpt = true
|
||||
|
|
|
@ -31,9 +31,9 @@ actor FervorController {
|
|||
private var lastSyncState = SyncState.done
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
init(instanceURL: URL, account: LocalData.Account?, session: URLSession = .shared) async {
|
||||
init(instanceURL: URL, account: LocalData.Account?) async {
|
||||
self.instanceURL = instanceURL
|
||||
self.client = FervorClient(instanceURL: instanceURL, accessToken: account?.token.accessToken, session: session)
|
||||
self.client = FervorClient(instanceURL: instanceURL, accessToken: account?.token.accessToken)
|
||||
self.account = account
|
||||
self.clientID = account?.clientID
|
||||
self.clientSecret = account?.clientSecret
|
||||
|
@ -45,8 +45,8 @@ actor FervorController {
|
|||
}
|
||||
}
|
||||
|
||||
convenience init(account: LocalData.Account, session: URLSession = .shared) async {
|
||||
await self.init(instanceURL: account.instanceURL, account: account, session: session)
|
||||
convenience init(account: LocalData.Account) async {
|
||||
await self.init(instanceURL: account.instanceURL, account: account)
|
||||
}
|
||||
|
||||
private func setSyncState(_ state: SyncState) {
|
||||
|
@ -66,35 +66,31 @@ actor FervorController {
|
|||
}
|
||||
|
||||
func syncAll() async throws {
|
||||
guard lastSyncState.isFinished else {
|
||||
guard lastSyncState == .done else {
|
||||
return
|
||||
}
|
||||
// always return to .done, even if we throw and stop syncing early
|
||||
defer { setSyncState(.done) }
|
||||
|
||||
do {
|
||||
setSyncState(.groupsAndFeeds)
|
||||
setSyncState(.groupsAndFeeds)
|
||||
|
||||
logger.info("Syncing groups and feeds")
|
||||
async let groups = try client.groups()
|
||||
async let feeds = try client.feeds()
|
||||
try await persistentContainer.sync(serverGroups: groups, serverFeeds: feeds)
|
||||
logger.info("Syncing groups and feeds")
|
||||
async let groups = try client.groups()
|
||||
async let feeds = try client.feeds()
|
||||
try await persistentContainer.sync(serverGroups: groups, serverFeeds: feeds)
|
||||
|
||||
setSyncState(.items)
|
||||
setSyncState(.items)
|
||||
|
||||
let lastSync = try await persistentContainer.lastSyncDate()
|
||||
logger.info("Syncing items with last sync date: \(String(describing: lastSync), privacy: .public)")
|
||||
let update = try await client.syncItems(lastSync: lastSync)
|
||||
try await persistentContainer.syncItems(update, setProgress: { count, total in self.setSyncState(.updateItems(current: count, total: total)) })
|
||||
try await persistentContainer.updateLastSyncDate(update.syncTimestamp)
|
||||
let lastSync = try await persistentContainer.lastSyncDate()
|
||||
logger.info("Syncing items with last sync date: \(String(describing: lastSync), privacy: .public)")
|
||||
let update = try await client.syncItems(lastSync: lastSync)
|
||||
try await persistentContainer.syncItems(update, setProgress: { count, total in self.setSyncState(.updateItems(current: count, total: total)) })
|
||||
try await persistentContainer.updateLastSyncDate(update.syncTimestamp)
|
||||
|
||||
await ExcerptGenerator.generateAll(self, setProgress: { count, total in self.setSyncState(.excerpts(current: count, total: total)) })
|
||||
setSyncState(.excerpts)
|
||||
await ExcerptGenerator.generateAll(self)
|
||||
|
||||
await WidgetHelper.updateWidgetData(fervorController: self)
|
||||
|
||||
setSyncState(.done)
|
||||
} catch {
|
||||
setSyncState(.error(error))
|
||||
throw error
|
||||
}
|
||||
await WidgetHelper.updateWidgetData(fervorController: self)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
|
@ -174,21 +170,11 @@ actor FervorController {
|
|||
}
|
||||
|
||||
extension FervorController {
|
||||
enum SyncState {
|
||||
enum SyncState: Equatable {
|
||||
case groupsAndFeeds
|
||||
case items
|
||||
case updateItems(current: Int, total: Int)
|
||||
case excerpts(current: Int, total: Int)
|
||||
case error(Error)
|
||||
case excerpts
|
||||
case done
|
||||
|
||||
var isFinished: Bool {
|
||||
switch self {
|
||||
case .error(_), .done:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,13 +37,5 @@
|
|||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
||||
<array>
|
||||
<string>net.shadowfacts.Reader.refresh</string>
|
||||
</array>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>fetch</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
@ -47,9 +47,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||
fervorController = await FervorController(account: account)
|
||||
syncFromServer()
|
||||
createAppUI()
|
||||
if !connectionOptions.urlContexts.isEmpty {
|
||||
self.scene(scene, openURLContexts: connectionOptions.urlContexts)
|
||||
} else if let activity = activity {
|
||||
if let activity = activity {
|
||||
await setupUI(from: activity)
|
||||
}
|
||||
setupSceneActivationConditions()
|
||||
|
@ -92,6 +90,15 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||
func sceneWillResignActive(_ scene: UIScene) {
|
||||
// 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).
|
||||
|
||||
if let fervorController = fervorController {
|
||||
Task(priority: .userInitiated) {
|
||||
await fervorController.syncReadToServer()
|
||||
}
|
||||
Task(priority: .userInitiated) {
|
||||
await WidgetHelper.updateWidgetData(fervorController: fervorController)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sceneWillEnterForeground(_ scene: UIScene) {
|
||||
|
@ -103,17 +110,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||
// 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
|
||||
// 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? {
|
||||
|
|
|
@ -29,22 +29,13 @@ class HomeViewController: UIViewController {
|
|||
private var feedResultsController: NSFetchedResultsController<Feed>!
|
||||
|
||||
private var lastSyncState = FervorController.SyncState.done
|
||||
// weak so that when it's removed from the superview, this becomes nil
|
||||
private weak var syncStateView: SyncStateView?
|
||||
private var syncStateView: SyncStateView?
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
init(fervorController: FervorController) {
|
||||
self.fervorController = fervorController
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
fervorController.syncState
|
||||
.buffer(size: 25, prefetch: .byRequest, whenFull: .dropOldest)
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { [unowned self] in
|
||||
self.syncStateChanged($0)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
|
@ -102,6 +93,13 @@ class HomeViewController: UIViewController {
|
|||
feedResultsController = NSFetchedResultsController(fetchRequest: feedReq, managedObjectContext: fervorController.persistentContainer.viewContext, sectionNameKeyPath: nil, cacheName: nil)
|
||||
feedResultsController.delegate = self
|
||||
try! feedResultsController.performFetch()
|
||||
|
||||
fervorController.syncState
|
||||
.debounce(for: .milliseconds(250), scheduler: RunLoop.main, options: nil)
|
||||
.sink { [unowned self] in
|
||||
self.syncStateChanged($0)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
|
@ -152,7 +150,7 @@ class HomeViewController: UIViewController {
|
|||
}
|
||||
|
||||
private func syncStateChanged(_ newState: FervorController.SyncState) {
|
||||
if case .done = newState {
|
||||
if newState == .done {
|
||||
// update unread counts for visible items
|
||||
var snapshot = dataSource.snapshot()
|
||||
snapshot.reconfigureItems(snapshot.itemIdentifiers)
|
||||
|
@ -164,7 +162,7 @@ class HomeViewController: UIViewController {
|
|||
}
|
||||
}
|
||||
|
||||
func updateView(_ syncStateView: SyncStateView, isFirstUpdate: Bool) {
|
||||
func updateView(_ syncStateView: SyncStateView) {
|
||||
switch newState {
|
||||
case .groupsAndFeeds:
|
||||
syncStateView.label.text = "Syncing groups and feeds"
|
||||
|
@ -172,24 +170,21 @@ class HomeViewController: UIViewController {
|
|||
syncStateView.label.text = "Syncing items"
|
||||
case .updateItems(current: let current, total: let total):
|
||||
syncStateView.label.text = "Updating \(current + 1) of \(total) item\(total == 1 ? "" : "s")"
|
||||
case .excerpts(current: let current, total: let total):
|
||||
syncStateView.label.text = "Generating \(current) of \(total) excerpt\(total == 1 ? "" : "s")"
|
||||
case .error(let error):
|
||||
syncStateView.label.text = "Error syncing"
|
||||
syncStateView.subtitleLabel.isHidden = false
|
||||
syncStateView.subtitleLabel.text = error.localizedDescription
|
||||
|
||||
syncStateView.removeAfterDelay(delay: isFirstUpdate ? 2 : 1)
|
||||
|
||||
case .excerpts:
|
||||
syncStateView.label.text = "Generating excerpts"
|
||||
case .done:
|
||||
syncStateView.label.text = "Done syncing"
|
||||
|
||||
syncStateView.removeAfterDelay(delay: isFirstUpdate ? 2 : 1)
|
||||
UIView.animate(withDuration: 0.25, delay: 1, options: .curveEaseIn) {
|
||||
syncStateView.transform = CGAffineTransform(translationX: 0, y: syncStateView.bounds.height)
|
||||
} completion: { _ in
|
||||
syncStateView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let syncStateView = self.syncStateView {
|
||||
updateView(syncStateView, isFirstUpdate: false)
|
||||
updateView(syncStateView)
|
||||
} else {
|
||||
let syncStateView = SyncStateView()
|
||||
self.syncStateView = syncStateView
|
||||
|
@ -201,13 +196,13 @@ class HomeViewController: UIViewController {
|
|||
syncStateView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
])
|
||||
|
||||
updateView(syncStateView, isFirstUpdate: true)
|
||||
|
||||
view.layoutIfNeeded()
|
||||
syncStateView.transform = CGAffineTransform(translationX: 0, y: syncStateView.bounds.height)
|
||||
|
||||
UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseOut) {
|
||||
syncStateView.transform = .identity
|
||||
} completion: { _ in
|
||||
updateView(syncStateView)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -340,7 +335,6 @@ extension HomeViewController: LoginViewControllerDelegate {
|
|||
|
||||
private class SyncStateView: UIView {
|
||||
let label = UILabel()
|
||||
let subtitleLabel = UILabel()
|
||||
|
||||
init() {
|
||||
super.init(frame: .zero)
|
||||
|
@ -350,20 +344,8 @@ private class SyncStateView: UIView {
|
|||
blurView.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(blurView)
|
||||
|
||||
label.font = .preferredFont(forTextStyle: .callout)
|
||||
subtitleLabel.font = .preferredFont(forTextStyle: .caption1)
|
||||
subtitleLabel.isHidden = true
|
||||
subtitleLabel.numberOfLines = 2
|
||||
|
||||
let stack = UIStackView(arrangedSubviews: [
|
||||
label,
|
||||
subtitleLabel,
|
||||
])
|
||||
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||
stack.axis = .vertical
|
||||
stack.alignment = .center
|
||||
stack.spacing = 4
|
||||
addSubview(stack)
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(label)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
blurView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
|
@ -371,12 +353,10 @@ private class SyncStateView: UIView {
|
|||
blurView.topAnchor.constraint(equalTo: topAnchor),
|
||||
blurView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
|
||||
stack.centerXAnchor.constraint(equalTo: safeAreaLayoutGuide.centerXAnchor),
|
||||
stack.topAnchor.constraint(greaterThanOrEqualToSystemSpacingBelow: safeAreaLayoutGuide.topAnchor, multiplier: 1),
|
||||
stack.bottomAnchor.constraint(lessThanOrEqualToSystemSpacingBelow: safeAreaLayoutGuide.bottomAnchor, multiplier: 1),
|
||||
stack.centerYAnchor.constraint(equalTo: safeAreaLayoutGuide.centerYAnchor),
|
||||
label.centerXAnchor.constraint(equalTo: safeAreaLayoutGuide.centerXAnchor),
|
||||
label.centerYAnchor.constraint(equalTo: safeAreaLayoutGuide.centerYAnchor),
|
||||
|
||||
topAnchor.constraint(lessThanOrEqualTo: safeAreaLayoutGuide.bottomAnchor, constant: -50),
|
||||
safeAreaLayoutGuide.heightAnchor.constraint(equalToConstant: 50),
|
||||
])
|
||||
|
||||
layer.shadowColor = UIColor.black.cgColor
|
||||
|
@ -387,15 +367,4 @@ private class SyncStateView: UIView {
|
|||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func removeAfterDelay(delay: Int) {
|
||||
// can't use UIView.animate's delay b/c it may clash with the appearance animation
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(delay)) {
|
||||
UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseIn) {
|
||||
self.transform = CGAffineTransform(translationX: 0, y: self.bounds.height)
|
||||
} completion: { _ in
|
||||
self.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -81,12 +81,10 @@ class ReadViewController: UIViewController {
|
|||
webView.scrollView.alwaysBounceHorizontal = false
|
||||
view.addSubview(webView)
|
||||
|
||||
// subtract 0.5, because otherwise, on ipad, the web view's scroll content view ends up being wider than the scroll view itself, causing the content to bounce horizontally
|
||||
let webViewWidthFix = UIDevice.current.userInterfaceIdiom == .pad ? -0.5 : 0
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
webView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
webView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: webViewWidthFix),
|
||||
// subtract 0.5, because otherwise, on ipad, the web view's scroll content view ends up being wider than the scroll view itself, causing the content to bounce horizontally
|
||||
webView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -0.5),
|
||||
webView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
webView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
])
|
||||
|
|
|
@ -8,9 +8,6 @@
|
|||
import Foundation
|
||||
import WidgetKit
|
||||
import Persistence
|
||||
import OSLog
|
||||
|
||||
private let logger = Logger(subsystem: "net.shadowfacts.Reader", category: "WidgetHelper")
|
||||
|
||||
struct WidgetHelper {
|
||||
private init() {}
|
||||
|
@ -20,13 +17,12 @@ struct WidgetHelper {
|
|||
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
|
||||
|
||||
let context = fervorController.persistentContainer.backgroundContext
|
||||
let prioritizedItems: [WidgetData.Item] = await context.perform {
|
||||
let prioritizedItems: [WidgetData.Item] = await fervorController.persistentContainer.performBackgroundTask { ctx in
|
||||
let req = Item.fetchRequest()
|
||||
req.sortDescriptors = [NSSortDescriptor(key: "published", ascending: false)]
|
||||
req.fetchLimit = 32
|
||||
req.predicate = NSPredicate(format: "read = NO")
|
||||
var items = (try? context.fetch(req)) ?? []
|
||||
var items = (try? ctx.fetch(req)) ?? []
|
||||
|
||||
var prioritizedItems: [Item] = []
|
||||
|
||||
|
|
Loading…
Reference in New Issue