Add sync progress indicator
This commit is contained in:
parent
f320311a78
commit
c41a81414d
|
@ -102,7 +102,7 @@ class PersistentContainer: NSPersistentContainer {
|
|||
try await self.saveViewContext()
|
||||
}
|
||||
|
||||
func syncItems(_ syncUpdate: ItemsSyncUpdate) async throws {
|
||||
func syncItems(_ syncUpdate: ItemsSyncUpdate, setSyncState: @escaping (FervorController.SyncState) -> Void) async throws {
|
||||
try await backgroundContext.perform {
|
||||
self.logger.debug("syncItems: deleting \(syncUpdate.delete.count, privacy: .public) items")
|
||||
let deleteReq = Item.fetchRequest()
|
||||
|
@ -122,7 +122,8 @@ class PersistentContainer: NSPersistentContainer {
|
|||
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 item in syncUpdate.upsert {
|
||||
for (index, item) in syncUpdate.upsert.enumerated() {
|
||||
setSyncState(.updateItems(current: index, total: syncUpdate.upsert.count))
|
||||
if let existing = existing.first(where: { $0.id == item.id }) {
|
||||
existing.updateFromServer(item)
|
||||
} else {
|
||||
|
|
|
@ -15,12 +15,13 @@ public struct ExcerptGenerator {
|
|||
|
||||
private static let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "ExcerptGenerator")
|
||||
|
||||
static func generateAll(_ fervorController: FervorController) {
|
||||
static func generateAll(_ fervorController: FervorController) async {
|
||||
let req = Item.fetchRequest()
|
||||
req.predicate = NSPredicate(format: "generatedExcerpt = NO")
|
||||
req.sortDescriptors = [NSSortDescriptor(key: "published", ascending: false)]
|
||||
req.fetchBatchSize = 50
|
||||
fervorController.persistentContainer.performBackgroundTask { ctx in
|
||||
let ctx = fervorController.persistentContainer.newBackgroundContext()
|
||||
await ctx.perform {
|
||||
guard let items = try? ctx.fetch(req) else { return }
|
||||
var count = 0
|
||||
for item in items {
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import Foundation
|
||||
import Fervor
|
||||
import OSLog
|
||||
import Combine
|
||||
|
||||
class FervorController {
|
||||
|
||||
|
@ -25,6 +26,8 @@ class FervorController {
|
|||
|
||||
private(set) var persistentContainer: PersistentContainer!
|
||||
|
||||
let syncStateSubject = PassthroughSubject<SyncState, Never>()
|
||||
|
||||
init(instanceURL: URL) {
|
||||
self.instanceURL = instanceURL
|
||||
self.client = FervorClient(instanceURL: instanceURL, accessToken: nil)
|
||||
|
@ -42,6 +45,12 @@ class FervorController {
|
|||
self.persistentContainer = PersistentContainer(account: account, fervorController: self)
|
||||
}
|
||||
|
||||
private func setSyncState(_ state: SyncState) {
|
||||
DispatchQueue.main.async {
|
||||
self.syncStateSubject.send(state)
|
||||
}
|
||||
}
|
||||
|
||||
func register() async throws -> ClientRegistration {
|
||||
let registration = try await client.register(clientName: "Frenzy iOS", website: nil, redirectURI: FervorController.oauthRedirectURI)
|
||||
clientID = registration.clientID
|
||||
|
@ -56,16 +65,25 @@ class FervorController {
|
|||
}
|
||||
|
||||
func syncAll() async throws {
|
||||
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)
|
||||
|
||||
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)
|
||||
try await persistentContainer.syncItems(update, setSyncState: setSyncState(_:))
|
||||
try await persistentContainer.updateLastSyncDate(update.syncTimestamp)
|
||||
|
||||
setSyncState(.excerpts)
|
||||
await ExcerptGenerator.generateAll(self)
|
||||
|
||||
setSyncState(.done)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
|
@ -132,3 +150,13 @@ class FervorController {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
extension FervorController {
|
||||
enum SyncState: Equatable {
|
||||
case groupsAndFeeds
|
||||
case items
|
||||
case updateItems(current: Int, total: Int)
|
||||
case excerpts
|
||||
case done
|
||||
}
|
||||
}
|
||||
|
|
|
@ -111,12 +111,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||
}
|
||||
Task(priority: .userInitiated) {
|
||||
do {
|
||||
try await self.fervorController.syncAll()
|
||||
try await fervorController.syncAll()
|
||||
} catch {
|
||||
logger.error("Unable to sync from server: \(String(describing: error), privacy: .public)")
|
||||
}
|
||||
|
||||
ExcerptGenerator.generateAll(fervorController)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import UIKit
|
||||
import CoreData
|
||||
import Combine
|
||||
|
||||
protocol HomeViewControllerDelegate: AnyObject {
|
||||
func switchToAccount(_ account: LocalData.Account)
|
||||
|
@ -26,6 +27,10 @@ class HomeViewController: UIViewController {
|
|||
private var groupResultsController: NSFetchedResultsController<Group>!
|
||||
private var feedResultsController: NSFetchedResultsController<Feed>!
|
||||
|
||||
private var lastSyncState = FervorController.SyncState.done
|
||||
private var syncStateView: SyncStateView?
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
init(fervorController: FervorController) {
|
||||
self.fervorController = fervorController
|
||||
|
||||
|
@ -87,6 +92,14 @@ class HomeViewController: UIViewController {
|
|||
feedResultsController = NSFetchedResultsController(fetchRequest: feedReq, managedObjectContext: fervorController.persistentContainer.viewContext, sectionNameKeyPath: nil, cacheName: nil)
|
||||
feedResultsController.delegate = self
|
||||
try! feedResultsController.performFetch()
|
||||
|
||||
fervorController.syncStateSubject
|
||||
.delay(for: .milliseconds(500), tolerance: nil, scheduler: RunLoop.main, options: nil)
|
||||
.debounce(for: .milliseconds(250), scheduler: RunLoop.main, options: nil)
|
||||
.sink { [unowned self] in
|
||||
self.syncStateChanged($0)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
|
@ -136,6 +149,56 @@ class HomeViewController: UIViewController {
|
|||
return dataSource
|
||||
}
|
||||
|
||||
private func syncStateChanged(_ newState: FervorController.SyncState) {
|
||||
if newState == .done && syncStateView == nil {
|
||||
return
|
||||
}
|
||||
|
||||
func updateView(_ syncStateView: SyncStateView) {
|
||||
switch newState {
|
||||
case .groupsAndFeeds:
|
||||
syncStateView.label.text = "Syncing groups and feeds"
|
||||
case .items:
|
||||
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:
|
||||
syncStateView.label.text = "Generating excerpts"
|
||||
case .done:
|
||||
syncStateView.label.text = "Done syncing"
|
||||
|
||||
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)
|
||||
} else {
|
||||
let syncStateView = SyncStateView()
|
||||
self.syncStateView = syncStateView
|
||||
syncStateView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(syncStateView)
|
||||
NSLayoutConstraint.activate([
|
||||
syncStateView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
syncStateView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
syncStateView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
])
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension HomeViewController {
|
||||
|
@ -307,3 +370,39 @@ extension HomeViewController: LoginViewControllerDelegate {
|
|||
(view.window!.windowScene!.delegate as! SceneDelegate).didLogin(with: controller)
|
||||
}
|
||||
}
|
||||
|
||||
private class SyncStateView: UIView {
|
||||
let label = UILabel()
|
||||
|
||||
init() {
|
||||
super.init(frame: .zero)
|
||||
|
||||
let blur = UIBlurEffect(style: .regular)
|
||||
let blurView = UIVisualEffectView(effect: blur)
|
||||
blurView.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(blurView)
|
||||
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(label)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
blurView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
blurView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
blurView.topAnchor.constraint(equalTo: topAnchor),
|
||||
blurView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
|
||||
label.centerXAnchor.constraint(equalTo: safeAreaLayoutGuide.centerXAnchor),
|
||||
label.centerYAnchor.constraint(equalTo: safeAreaLayoutGuide.centerYAnchor),
|
||||
|
||||
safeAreaLayoutGuide.heightAnchor.constraint(equalToConstant: 50),
|
||||
])
|
||||
|
||||
layer.shadowColor = UIColor.black.cgColor
|
||||
layer.shadowOpacity = 0.1
|
||||
layer.shadowRadius = 10
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue