Sync state progress improvements

This commit is contained in:
Shadowfacts 2022-06-25 18:40:56 -04:00
parent 6cd2bf248d
commit 307299dd4d
3 changed files with 96 additions and 50 deletions

View File

@ -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

View File

@ -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
}
}
}
}

View File

@ -29,13 +29,22 @@ class HomeViewController: UIViewController {
private var feedResultsController: NSFetchedResultsController<Feed>!
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<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) {
@ -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()
}
}
}
}