Sync state progress improvements
This commit is contained in:
parent
6cd2bf248d
commit
307299dd4d
|
@ -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
|
||||
|
|
|
@ -66,12 +66,11 @@ 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) }
|
||||
|
||||
do {
|
||||
setSyncState(.groupsAndFeeds)
|
||||
|
||||
logger.info("Syncing groups and feeds")
|
||||
|
@ -87,10 +86,15 @@ actor FervorController {
|
|||
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 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue