From 307299dd4d17d6bc6691d2dbf8dcf27008194d37 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sat, 25 Jun 2022 18:40:56 -0400 Subject: [PATCH] Sync state progress improvements --- Reader/ExcerptGenerator.swift | 3 +- Reader/FervorController.swift | 62 +++++++++------ Reader/Screens/Home/HomeViewController.swift | 81 ++++++++++++++------ 3 files changed, 96 insertions(+), 50 deletions(-) diff --git a/Reader/ExcerptGenerator.swift b/Reader/ExcerptGenerator.swift index a6273b7..1b6a036 100644 --- a/Reader/ExcerptGenerator.swift +++ b/Reader/ExcerptGenerator.swift @@ -16,7 +16,7 @@ public struct ExcerptGenerator { private static let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "ExcerptGenerator") - static func generateAll(_ fervorController: FervorController) async { + static func generateAll(_ fervorController: FervorController, setProgress: @escaping (_ current: Int, _ total: Int) -> Void) async { let req = Item.fetchRequest() req.predicate = NSPredicate(format: "generatedExcerpt = NO") req.sortDescriptors = [NSSortDescriptor(key: "published", ascending: false)] @@ -31,6 +31,7 @@ public struct ExcerptGenerator { count += 1 if count % 50 == 0 { logger.debug("Generated \(count, privacy: .public) excerpts") + setProgress(count, items.count) } } item.generatedExcerpt = true diff --git a/Reader/FervorController.swift b/Reader/FervorController.swift index 7db0831..fb4f95d 100644 --- a/Reader/FervorController.swift +++ b/Reader/FervorController.swift @@ -66,31 +66,35 @@ actor FervorController { } func syncAll() async throws { - guard lastSyncState == .done else { + guard lastSyncState.isFinished else { return } - // always return to .done, even if we throw and stop syncing early - defer { setSyncState(.done) } - 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, setProgress: { count, total in self.setSyncState(.updateItems(current: count, total: total)) }) - try await persistentContainer.updateLastSyncDate(update.syncTimestamp) - - setSyncState(.excerpts) - await ExcerptGenerator.generateAll(self) - - await WidgetHelper.updateWidgetData(fervorController: self) + do { + 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, 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)) }) + + await WidgetHelper.updateWidgetData(fervorController: self) + + setSyncState(.done) + } catch { + setSyncState(.error(error)) + throw error + } } @MainActor @@ -170,11 +174,21 @@ actor FervorController { } extension FervorController { - enum SyncState: Equatable { + enum SyncState { case groupsAndFeeds case items case updateItems(current: Int, total: Int) - case excerpts + case excerpts(current: Int, total: Int) + case error(Error) case done + + var isFinished: Bool { + switch self { + case .error(_), .done: + return true + default: + return false + } + } } } diff --git a/Reader/Screens/Home/HomeViewController.swift b/Reader/Screens/Home/HomeViewController.swift index 4840781..69c53f9 100644 --- a/Reader/Screens/Home/HomeViewController.swift +++ b/Reader/Screens/Home/HomeViewController.swift @@ -29,13 +29,22 @@ class HomeViewController: UIViewController { private var feedResultsController: NSFetchedResultsController! private var lastSyncState = FervorController.SyncState.done - private var syncStateView: SyncStateView? + // weak so that when it's removed from the superview, this becomes nil + private weak var syncStateView: SyncStateView? private var cancellables = Set() 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) { @@ -93,13 +102,6 @@ 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) { @@ -150,7 +152,7 @@ class HomeViewController: UIViewController { } private func syncStateChanged(_ newState: FervorController.SyncState) { - if newState == .done { + if case .done = newState { // update unread counts for visible items var snapshot = dataSource.snapshot() snapshot.reconfigureItems(snapshot.itemIdentifiers) @@ -162,7 +164,7 @@ class HomeViewController: UIViewController { } } - func updateView(_ syncStateView: SyncStateView) { + func updateView(_ syncStateView: SyncStateView, isFirstUpdate: Bool) { switch newState { case .groupsAndFeeds: syncStateView.label.text = "Syncing groups and feeds" @@ -170,21 +172,24 @@ 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: - syncStateView.label.text = "Generating excerpts" + 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 .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() - } + syncStateView.removeAfterDelay(delay: isFirstUpdate ? 2 : 1) } } if let syncStateView = self.syncStateView { - updateView(syncStateView) + updateView(syncStateView, isFirstUpdate: false) } else { let syncStateView = SyncStateView() self.syncStateView = syncStateView @@ -196,13 +201,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) } } } @@ -335,6 +340,7 @@ extension HomeViewController: LoginViewControllerDelegate { private class SyncStateView: UIView { let label = UILabel() + let subtitleLabel = UILabel() init() { super.init(frame: .zero) @@ -344,8 +350,20 @@ private class SyncStateView: UIView { blurView.translatesAutoresizingMaskIntoConstraints = false addSubview(blurView) - label.translatesAutoresizingMaskIntoConstraints = false - addSubview(label) + 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) NSLayoutConstraint.activate([ blurView.leadingAnchor.constraint(equalTo: leadingAnchor), @@ -353,10 +371,12 @@ private class SyncStateView: UIView { blurView.topAnchor.constraint(equalTo: topAnchor), blurView.bottomAnchor.constraint(equalTo: bottomAnchor), - label.centerXAnchor.constraint(equalTo: safeAreaLayoutGuide.centerXAnchor), - label.centerYAnchor.constraint(equalTo: safeAreaLayoutGuide.centerYAnchor), + 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), - safeAreaLayoutGuide.heightAnchor.constraint(equalToConstant: 50), + topAnchor.constraint(lessThanOrEqualTo: safeAreaLayoutGuide.bottomAnchor, constant: -50), ]) layer.shadowColor = UIColor.black.cgColor @@ -367,4 +387,15 @@ 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() + } + } + } }