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")
|
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()
|
let req = Item.fetchRequest()
|
||||||
req.predicate = NSPredicate(format: "generatedExcerpt = NO")
|
req.predicate = NSPredicate(format: "generatedExcerpt = NO")
|
||||||
req.sortDescriptors = [NSSortDescriptor(key: "published", ascending: false)]
|
req.sortDescriptors = [NSSortDescriptor(key: "published", ascending: false)]
|
||||||
|
@ -31,6 +31,7 @@ public struct ExcerptGenerator {
|
||||||
count += 1
|
count += 1
|
||||||
if count % 50 == 0 {
|
if count % 50 == 0 {
|
||||||
logger.debug("Generated \(count, privacy: .public) excerpts")
|
logger.debug("Generated \(count, privacy: .public) excerpts")
|
||||||
|
setProgress(count, items.count)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
item.generatedExcerpt = true
|
item.generatedExcerpt = true
|
||||||
|
|
|
@ -66,12 +66,11 @@ actor FervorController {
|
||||||
}
|
}
|
||||||
|
|
||||||
func syncAll() async throws {
|
func syncAll() async throws {
|
||||||
guard lastSyncState == .done else {
|
guard lastSyncState.isFinished else {
|
||||||
return
|
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")
|
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.syncItems(update, setProgress: { count, total in self.setSyncState(.updateItems(current: count, total: total)) })
|
||||||
try await persistentContainer.updateLastSyncDate(update.syncTimestamp)
|
try await persistentContainer.updateLastSyncDate(update.syncTimestamp)
|
||||||
|
|
||||||
setSyncState(.excerpts)
|
await ExcerptGenerator.generateAll(self, setProgress: { count, total in self.setSyncState(.excerpts(current: count, total: total)) })
|
||||||
await ExcerptGenerator.generateAll(self)
|
|
||||||
|
|
||||||
await WidgetHelper.updateWidgetData(fervorController: self)
|
await WidgetHelper.updateWidgetData(fervorController: self)
|
||||||
|
|
||||||
|
setSyncState(.done)
|
||||||
|
} catch {
|
||||||
|
setSyncState(.error(error))
|
||||||
|
throw error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
|
@ -170,11 +174,21 @@ actor FervorController {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension FervorController {
|
extension FervorController {
|
||||||
enum SyncState: Equatable {
|
enum SyncState {
|
||||||
case groupsAndFeeds
|
case groupsAndFeeds
|
||||||
case items
|
case items
|
||||||
case updateItems(current: Int, total: Int)
|
case updateItems(current: Int, total: Int)
|
||||||
case excerpts
|
case excerpts(current: Int, total: Int)
|
||||||
|
case error(Error)
|
||||||
case done
|
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 feedResultsController: NSFetchedResultsController<Feed>!
|
||||||
|
|
||||||
private var lastSyncState = FervorController.SyncState.done
|
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>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
init(fervorController: FervorController) {
|
init(fervorController: FervorController) {
|
||||||
self.fervorController = fervorController
|
self.fervorController = fervorController
|
||||||
|
|
||||||
super.init(nibName: nil, bundle: nil)
|
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) {
|
required init?(coder: NSCoder) {
|
||||||
|
@ -93,13 +102,6 @@ class HomeViewController: UIViewController {
|
||||||
feedResultsController = NSFetchedResultsController(fetchRequest: feedReq, managedObjectContext: fervorController.persistentContainer.viewContext, sectionNameKeyPath: nil, cacheName: nil)
|
feedResultsController = NSFetchedResultsController(fetchRequest: feedReq, managedObjectContext: fervorController.persistentContainer.viewContext, sectionNameKeyPath: nil, cacheName: nil)
|
||||||
feedResultsController.delegate = self
|
feedResultsController.delegate = self
|
||||||
try! feedResultsController.performFetch()
|
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) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
@ -150,7 +152,7 @@ class HomeViewController: UIViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func syncStateChanged(_ newState: FervorController.SyncState) {
|
private func syncStateChanged(_ newState: FervorController.SyncState) {
|
||||||
if newState == .done {
|
if case .done = newState {
|
||||||
// update unread counts for visible items
|
// update unread counts for visible items
|
||||||
var snapshot = dataSource.snapshot()
|
var snapshot = dataSource.snapshot()
|
||||||
snapshot.reconfigureItems(snapshot.itemIdentifiers)
|
snapshot.reconfigureItems(snapshot.itemIdentifiers)
|
||||||
|
@ -162,7 +164,7 @@ class HomeViewController: UIViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateView(_ syncStateView: SyncStateView) {
|
func updateView(_ syncStateView: SyncStateView, isFirstUpdate: Bool) {
|
||||||
switch newState {
|
switch newState {
|
||||||
case .groupsAndFeeds:
|
case .groupsAndFeeds:
|
||||||
syncStateView.label.text = "Syncing groups and feeds"
|
syncStateView.label.text = "Syncing groups and feeds"
|
||||||
|
@ -170,21 +172,24 @@ class HomeViewController: UIViewController {
|
||||||
syncStateView.label.text = "Syncing items"
|
syncStateView.label.text = "Syncing items"
|
||||||
case .updateItems(current: let current, total: let total):
|
case .updateItems(current: let current, total: let total):
|
||||||
syncStateView.label.text = "Updating \(current + 1) of \(total) item\(total == 1 ? "" : "s")"
|
syncStateView.label.text = "Updating \(current + 1) of \(total) item\(total == 1 ? "" : "s")"
|
||||||
case .excerpts:
|
case .excerpts(current: let current, total: let total):
|
||||||
syncStateView.label.text = "Generating excerpts"
|
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:
|
case .done:
|
||||||
syncStateView.label.text = "Done syncing"
|
syncStateView.label.text = "Done syncing"
|
||||||
|
|
||||||
UIView.animate(withDuration: 0.25, delay: 1, options: .curveEaseIn) {
|
syncStateView.removeAfterDelay(delay: isFirstUpdate ? 2 : 1)
|
||||||
syncStateView.transform = CGAffineTransform(translationX: 0, y: syncStateView.bounds.height)
|
|
||||||
} completion: { _ in
|
|
||||||
syncStateView.removeFromSuperview()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let syncStateView = self.syncStateView {
|
if let syncStateView = self.syncStateView {
|
||||||
updateView(syncStateView)
|
updateView(syncStateView, isFirstUpdate: false)
|
||||||
} else {
|
} else {
|
||||||
let syncStateView = SyncStateView()
|
let syncStateView = SyncStateView()
|
||||||
self.syncStateView = syncStateView
|
self.syncStateView = syncStateView
|
||||||
|
@ -196,13 +201,13 @@ class HomeViewController: UIViewController {
|
||||||
syncStateView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
syncStateView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
updateView(syncStateView, isFirstUpdate: true)
|
||||||
|
|
||||||
view.layoutIfNeeded()
|
view.layoutIfNeeded()
|
||||||
syncStateView.transform = CGAffineTransform(translationX: 0, y: syncStateView.bounds.height)
|
syncStateView.transform = CGAffineTransform(translationX: 0, y: syncStateView.bounds.height)
|
||||||
|
|
||||||
UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseOut) {
|
UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseOut) {
|
||||||
syncStateView.transform = .identity
|
syncStateView.transform = .identity
|
||||||
} completion: { _ in
|
|
||||||
updateView(syncStateView)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -335,6 +340,7 @@ extension HomeViewController: LoginViewControllerDelegate {
|
||||||
|
|
||||||
private class SyncStateView: UIView {
|
private class SyncStateView: UIView {
|
||||||
let label = UILabel()
|
let label = UILabel()
|
||||||
|
let subtitleLabel = UILabel()
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
super.init(frame: .zero)
|
super.init(frame: .zero)
|
||||||
|
@ -344,8 +350,20 @@ private class SyncStateView: UIView {
|
||||||
blurView.translatesAutoresizingMaskIntoConstraints = false
|
blurView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
addSubview(blurView)
|
addSubview(blurView)
|
||||||
|
|
||||||
label.translatesAutoresizingMaskIntoConstraints = false
|
label.font = .preferredFont(forTextStyle: .callout)
|
||||||
addSubview(label)
|
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([
|
NSLayoutConstraint.activate([
|
||||||
blurView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
blurView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
@ -353,10 +371,12 @@ private class SyncStateView: UIView {
|
||||||
blurView.topAnchor.constraint(equalTo: topAnchor),
|
blurView.topAnchor.constraint(equalTo: topAnchor),
|
||||||
blurView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
blurView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
|
|
||||||
label.centerXAnchor.constraint(equalTo: safeAreaLayoutGuide.centerXAnchor),
|
stack.centerXAnchor.constraint(equalTo: safeAreaLayoutGuide.centerXAnchor),
|
||||||
label.centerYAnchor.constraint(equalTo: safeAreaLayoutGuide.centerYAnchor),
|
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
|
layer.shadowColor = UIColor.black.cgColor
|
||||||
|
@ -367,4 +387,15 @@ private class SyncStateView: UIView {
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
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