1
0
mirror of https://github.com/mastodon/mastodon-ios.git synced 2025-02-02 10:27:08 +01:00

Improve "Load more"-times when scrolling to the end (IOS-272) (#1303)

This is basically a refactoring of the `ListBatchFetchViewModel`, it
does two things:

1. Remove the "Reload this every 30 seconds", which caused a delay on
several screens
2. When users reach the bottom of the ScrollView, new content is
requested
3. DIY: Use this mechanism everywhere

The previous mechanism worked like this: Check every second, if the user
reached the end of the scrollView. If so: `shouldFetch`. As we increased
"every second" to "30 seconds", this caused a significant delay on all
screens which used this mechanism.

Others brought their own solution to fetch new content when users
reached THE END, like the HomeTimeline or the FollowerList. From now
one, there's a suggested way to deal with this "We must do something
once the user reachs the end!!!!"-issue.

P.S.: I'm not so happy with the `Self.`-approach and if someone has a
better name for `scrollViewDidScrollToEnd`, I'd be also thankful for a
hint. Maybe it's me, but I would have loved to add a new method to the
`UIScrollViewDelegate`-protocol (without Protocol-inheritance). Maybe
someone knows a way to do this? For now we'll leave it like this as the 
alternatives would add more complexity.
This commit is contained in:
Nathan Mattes 2024-05-29 08:35:56 +02:00 committed by GitHub
commit ef8ceb56c9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 166 additions and 284 deletions

View File

@ -365,7 +365,7 @@
DB6D9F3526351B7A008423CD /* NotificationService+Decrypt.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F3426351B7A008423CD /* NotificationService+Decrypt.swift */; };
DB72601C25E36A2100235243 /* MastodonServerRulesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */; };
DB72602725E36A6F00235243 /* MastodonServerRulesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */; };
DB7274F4273BB9B200577D95 /* ListBatchFetchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7274F3273BB9B200577D95 /* ListBatchFetchViewModel.swift */; };
DB7274F4273BB9B200577D95 /* UIScrollViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7274F3273BB9B200577D95 /* UIScrollViewDelegate.swift */; };
DB73B490261F030A002E9E9F /* SafariActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB73B48F261F030A002E9E9F /* SafariActivity.swift */; };
DB75BF1E263C1C1B00EDBF1F /* CustomScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB75BF1D263C1C1B00EDBF1F /* CustomScheduler.swift */; };
DB789A0B25F9F2950071ACA0 /* ComposeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */; };
@ -1045,7 +1045,7 @@
DB6D9F3426351B7A008423CD /* NotificationService+Decrypt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationService+Decrypt.swift"; sourceTree = "<group>"; };
DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewController.swift; sourceTree = "<group>"; };
DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewModel.swift; sourceTree = "<group>"; };
DB7274F3273BB9B200577D95 /* ListBatchFetchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListBatchFetchViewModel.swift; sourceTree = "<group>"; };
DB7274F3273BB9B200577D95 /* UIScrollViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIScrollViewDelegate.swift; sourceTree = "<group>"; };
DB73B48F261F030A002E9E9F /* SafariActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariActivity.swift; sourceTree = "<group>"; };
DB75BF1D263C1C1B00EDBF1F /* CustomScheduler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomScheduler.swift; sourceTree = "<group>"; };
DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewController.swift; sourceTree = "<group>"; };
@ -2524,27 +2524,28 @@
children = (
2A82294E29262EE000D2A1F7 /* AppContext+NextAccount.swift */,
5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */,
2A1FE47D2938C11200784BF1 /* Collection+IsNotEmpty.swift */,
2D206B8525F5FB0900143C56 /* Double.swift */,
DBB3BA2926A81C020004F2D4 /* FLAnimatedImageView.swift */,
DB68586325E619B700F0A850 /* NSKeyValueObservation.swift */,
2D939AB425EDD8A90076FA61 /* String.swift */,
DB68A06225E905E000CFDF14 /* UIApplication.swift */,
DB45FAB525CA5485005A8AC7 /* UIAlertController.swift */,
DB68A06225E905E000CFDF14 /* UIApplication.swift */,
DB45FAD625CA6C76005A8AC7 /* UIBarButtonItem.swift */,
2D206B9125F60EA700143C56 /* UIControl.swift */,
0FAA101B25E10E760017CCDE /* UIFont.swift */,
2AE244472927831100BDBF7C /* UIImage+SFSymbols.swift */,
855149C7295F1C5F00943D96 /* UIInterfaceOrientationMask.swift */,
DB9E0D6E25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift */,
2D84350425FF858100EECE90 /* UIScrollView.swift */,
DB7274F3273BB9B200577D95 /* UIScrollViewDelegate.swift */,
DBCC3B2F261440A50045B23D /* UITabBarController.swift */,
DB4481B825EE289600BEFB67 /* UITableView.swift */,
DBD376B1269302A4007FEC24 /* UITableViewCell.swift */,
0FAA101B25E10E760017CCDE /* UIFont.swift */,
2D206B9125F60EA700143C56 /* UIControl.swift */,
5DA732CB2629CEF500A92342 /* UIView+Remove.swift */,
2D24E1222626ED9D00A59D4F /* UIView+Gesture.swift */,
DB8AF55C25C138B7002E6C99 /* UIViewController.swift */,
2D3F9E0325DFA133004262D9 /* UITapGestureRecognizer.swift */,
2D84350425FF858100EECE90 /* UIScrollView.swift */,
DB9E0D6E25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift */,
2AE244472927831100BDBF7C /* UIImage+SFSymbols.swift */,
DBCC3B2F261440A50045B23D /* UITabBarController.swift */,
2A1FE47D2938C11200784BF1 /* Collection+IsNotEmpty.swift */,
855149C7295F1C5F00943D96 /* UIInterfaceOrientationMask.swift */,
2D24E1222626ED9D00A59D4F /* UIView+Gesture.swift */,
5DA732CB2629CEF500A92342 /* UIView+Remove.swift */,
DB8AF55C25C138B7002E6C99 /* UIViewController.swift */,
);
path = Extension;
sourceTree = "<group>";
@ -2676,7 +2677,6 @@
DB9D6C2025E502C60051B173 /* ViewModel */ = {
isa = PBXGroup;
children = (
DB7274F3273BB9B200577D95 /* ListBatchFetchViewModel.swift */,
);
path = ViewModel;
sourceTree = "<group>";
@ -3663,7 +3663,7 @@
DBB45B5927B39FE4002DC5A7 /* MediaPreviewVideoViewModel.swift in Sources */,
DB0FCB76279571C5006C02E2 /* ThreadViewController+DataSourceProvider.swift in Sources */,
DB1E346825F518E20079D7DF /* CategoryPickerSection.swift in Sources */,
DB7274F4273BB9B200577D95 /* ListBatchFetchViewModel.swift in Sources */,
DB7274F4273BB9B200577D95 /* UIScrollViewDelegate.swift in Sources */,
DB0618052785A73D0030EE79 /* RegisterItem.swift in Sources */,
DB6B74EF272FB55000C70B6E /* FollowerListViewController.swift in Sources */,
DB4AA6B327BA34B6009EC082 /* CellFrameCacheContainer.swift in Sources */,

View File

@ -0,0 +1,20 @@
// Copyright © 2024 Mastodon gGmbH. All rights reserved.
import UIKit
extension UIScrollViewDelegate {
static func scrollViewDidScrollToEnd(_ scrollView: UIScrollView, action: () -> Void) {
if scrollView.isDragging || scrollView.isTracking { return }
let frame = scrollView.frame
let contentOffset = scrollView.contentOffset
let contentSize = scrollView.contentSize
// if not enough content to fill the screen: don't do anything
if contentSize.height < frame.height { return }
if contentOffset.y > (contentSize.height - frame.height) {
action()
}
}
}

View File

@ -57,17 +57,6 @@ extension DiscoveryNewsViewController {
self.refreshControl.endRefreshing()
}
.store(in: &disposeBag)
// setup batch fetch
viewModel.listBatchFetchViewModel.setup(scrollView: tableView)
viewModel.listBatchFetchViewModel.shouldFetch
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
guard let self = self else { return }
guard self.view.window != nil else { return }
self.viewModel.stateMachine.enter(DiscoveryNewsViewModel.State.Loading.self)
}
.store(in: &disposeBag)
}
override func viewWillAppear(_ animated: Bool) {
@ -205,3 +194,13 @@ extension DiscoveryNewsViewController: TableViewControllerNavigateable {
}
}
//MARK: - UIScrollViewDelegate
extension DiscoveryNewsViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
Self.scrollViewDidScrollToEnd(scrollView) {
viewModel.stateMachine.enter(DiscoveryNewsViewModel.State.Loading.self)
}
}
}

View File

@ -20,7 +20,6 @@ final class DiscoveryNewsViewModel {
// input
let context: AppContext
let authContext: AuthContext
let listBatchFetchViewModel = ListBatchFetchViewModel()
// output
@Published var links: [Mastodon.Entity.Link] = []

View File

@ -74,17 +74,6 @@ extension DiscoveryPostsViewController {
self.refreshControl.endRefreshing()
}
.store(in: &disposeBag)
// setup batch fetch
viewModel.listBatchFetchViewModel.setup(scrollView: tableView)
viewModel.listBatchFetchViewModel.shouldFetch
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
guard let self = self else { return }
guard self.view.window != nil else { return }
self.viewModel.stateMachine.enter(DiscoveryPostsViewModel.State.Loading.self)
}
.store(in: &disposeBag)
}
override func viewWillAppear(_ animated: Bool) {
@ -171,3 +160,13 @@ extension DiscoveryPostsViewController: StatusTableViewControllerNavigateable {
statusKeyCommandHandler(sender)
}
}
//MARK: - UIScrollViewDelegate
extension DiscoveryPostsViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
Self.scrollViewDidScrollToEnd(scrollView) {
viewModel.stateMachine.enter(DiscoveryPostsViewModel.State.Loading.self)
}
}
}

View File

@ -21,7 +21,6 @@ final class DiscoveryPostsViewModel {
let context: AppContext
let authContext: AuthContext
let dataController: StatusDataController
let listBatchFetchViewModel = ListBatchFetchViewModel()
// output
var diffableDataSource: UITableViewDiffableDataSource<StatusSection, StatusItem>?

View File

@ -95,17 +95,7 @@ extension HashtagTimelineViewController {
self.refreshControl.endRefreshing()
}
.store(in: &disposeBag)
// setup batch fetch
viewModel.listBatchFetchViewModel.setup(scrollView: tableView)
viewModel.listBatchFetchViewModel.shouldFetch
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
guard let self = self else { return }
self.viewModel.stateMachine.enter(HashtagTimelineViewModel.State.Loading.self)
}
.store(in: &disposeBag)
viewModel.hashtagEntity
.receive(on: DispatchQueue.main)
.sink { [weak self] tag in
@ -257,3 +247,13 @@ extension HashtagTimelineViewController: StatusTableViewControllerNavigateable {
statusKeyCommandHandler(sender)
}
}
// MARK: - UIScrollViewDelegate
extension HashtagTimelineViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
Self.scrollViewDidScrollToEnd(scrollView) {
viewModel.stateMachine.enter(HashtagTimelineViewModel.State.Loading.self)
}
}
}

View File

@ -28,7 +28,6 @@ final class HashtagTimelineViewModel {
let isFetchingLatestTimeline = CurrentValueSubject<Bool, Never>(false)
let timelinePredicate = CurrentValueSubject<NSPredicate?, Never>(nil)
let hashtagEntity = CurrentValueSubject<Mastodon.Entity.Tag?, Never>(nil)
let listBatchFetchViewModel = ListBatchFetchViewModel()
// output
var diffableDataSource: UITableViewDiffableDataSource<StatusSection, StatusItem>?

View File

@ -192,18 +192,7 @@ extension HomeTimelineViewController {
statusTableViewCellDelegate: self,
timelineMiddleLoaderTableViewCellDelegate: self
)
// setup batch fetch
viewModel?.listBatchFetchViewModel.setup(scrollView: tableView)
viewModel?.listBatchFetchViewModel.shouldFetch
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
guard let self = self else { return }
guard self.view.window != nil else { return }
self.viewModel?.loadOldestStateMachine.enter(HomeTimelineViewModel.LoadOldestState.Loading.self)
}
.store(in: &disposeBag)
// bind refresh control
viewModel?.didLoadLatest
.receive(on: DispatchQueue.main)
@ -587,11 +576,22 @@ extension HomeTimelineViewController {
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
Self.scrollViewDidScrollToEnd(scrollView) {
guard let viewModel,
let currentState = viewModel.loadLatestStateMachine.currentState as? HomeTimelineViewModel.LoadLatestState,
(currentState.self is HomeTimelineViewModel.LoadLatestState.ContextSwitch) == false else { return }
viewModel.timelineDidReachEnd()
}
guard (scrollView.safeAreaInsets.top + scrollView.contentOffset.y) == 0 else {
return
}
hideTimelinePill()
}
private func savePositionBeforeScrollToTop() {
@ -673,16 +673,6 @@ extension HomeTimelineViewController: UITableViewDelegate, AutoGenerateTableView
}
// sourcery:end
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
guard let viewModel,
let currentState = viewModel.loadLatestStateMachine.currentState as? HomeTimelineViewModel.LoadLatestState,
(currentState.self is HomeTimelineViewModel.LoadLatestState.ContextSwitch) == false else { return }
if indexPath.row == tableView.numberOfRows(inSection: indexPath.section) - 1 {
viewModel.timelineDidReachEnd()
}
}
}
// MARK: - TimelineMiddleLoaderTableViewCellDelegate

View File

@ -25,7 +25,6 @@ final class HomeTimelineViewModel: NSObject {
let context: AppContext
let authContext: AuthContext
let dataController: FeedDataController
let listBatchFetchViewModel = ListBatchFetchViewModel()
var presentedSuggestions = false

View File

@ -54,17 +54,7 @@ extension NotificationTimelineViewController {
tableView: tableView,
notificationTableViewCellDelegate: self
)
// setup batch fetch
viewModel.listBatchFetchViewModel.setup(scrollView: tableView)
viewModel.listBatchFetchViewModel.shouldFetch
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
guard let self = self else { return }
self.viewModel.loadOldestStateMachine.enter(NotificationTimelineViewModel.LoadOldestState.Loading.self)
}
.store(in: &disposeBag)
// setup refresh control
tableView.refreshControl = refreshControl
viewModel.didLoadLatest
@ -306,3 +296,13 @@ extension NotificationTimelineViewController: TableViewControllerNavigateable {
}
}
//MARK: - UIScrollViewDelegate
extension NotificationTimelineViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
Self.scrollViewDidScrollToEnd(scrollView) {
viewModel.loadOldestStateMachine.enter(NotificationTimelineViewModel.LoadOldestState.Loading.self)
}
}
}

View File

@ -21,7 +21,6 @@ final class NotificationTimelineViewModel {
let authContext: AuthContext
let scope: Scope
let dataController: FeedDataController
let listBatchFetchViewModel = ListBatchFetchViewModel()
@Published var isLoadingLatest = false
@Published var lastAutomaticFetchTimestamp: Date?

View File

@ -58,16 +58,6 @@ extension BookmarkViewController {
tableView: tableView,
statusTableViewCellDelegate: self
)
// setup batch fetch
viewModel.listBatchFetchViewModel.setup(scrollView: tableView)
viewModel.listBatchFetchViewModel.shouldFetch
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
guard let self = self else { return }
self.viewModel.stateMachine.enter(BookmarkViewModel.State.Loading.self)
}
.store(in: &disposeBag)
}
override func viewWillAppear(_ animated: Bool) {
@ -75,13 +65,6 @@ extension BookmarkViewController {
tableView.deselectRow(with: transitionCoordinator, animated: animated)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
// aspectViewDidDisappear(animated)
}
}
// MARK: - UITableViewDelegate
@ -138,3 +121,13 @@ extension BookmarkViewController: StatusTableViewControllerNavigateable {
statusKeyCommandHandler(sender)
}
}
//MARK: - UIScrollViewDelegate
extension BookmarkViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
Self.scrollViewDidScrollToEnd(scrollView) {
viewModel.stateMachine.enter(BookmarkViewModel.State.Loading.self)
}
}
}

View File

@ -21,7 +21,6 @@ final class BookmarkViewModel {
let authContext: AuthContext
let dataController: StatusDataController
let listBatchFetchViewModel = ListBatchFetchViewModel()
// output
var diffableDataSource: UITableViewDiffableDataSource<StatusSection, StatusItem>?

View File

@ -61,16 +61,6 @@ extension FavoriteViewController {
tableView: tableView,
statusTableViewCellDelegate: self
)
// setup batch fetch
viewModel.listBatchFetchViewModel.setup(scrollView: tableView)
viewModel.listBatchFetchViewModel.shouldFetch
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
guard let self = self else { return }
self.viewModel.stateMachine.enter(FavoriteViewModel.State.Loading.self)
}
.store(in: &disposeBag)
}
override func viewWillAppear(_ animated: Bool) {
@ -141,3 +131,13 @@ extension FavoriteViewController: StatusTableViewControllerNavigateable {
statusKeyCommandHandler(sender)
}
}
//MARK: - UIScrollViewDelegate
extension FavoriteViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
Self.scrollViewDidScrollToEnd(scrollView) {
viewModel.stateMachine.enter(FavoriteViewModel.State.Loading.self)
}
}
}

View File

@ -20,7 +20,6 @@ final class FavoriteViewModel {
let context: AppContext
let authContext: AuthContext
let dataController: StatusDataController
let listBatchFetchViewModel = ListBatchFetchViewModel()
// output
var diffableDataSource: UITableViewDiffableDataSource<StatusSection, StatusItem>?

View File

@ -71,15 +71,6 @@ extension FollowerListViewController {
userTableViewCellDelegate: self
)
// setup batch fetch
viewModel.shouldFetch
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
guard let self = self else { return }
self.viewModel.stateMachine.enter(FollowerListViewModel.State.Loading.self)
}
.store(in: &disposeBag)
// trigger user timeline loading
Publishers.CombineLatest(
viewModel.$domain.removeDuplicates(),
@ -168,19 +159,8 @@ extension FollowerListViewController: DataSourceProvider {
extension FollowerListViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.isDragging || scrollView.isTracking { return }
let frame = scrollView.frame
let contentOffset = scrollView.contentOffset
let contentSize = scrollView.contentSize
let visibleBottomY = contentOffset.y + frame.height
let offset = 2 * frame.height
let fetchThrottleOffsetY = contentSize.height - offset
if visibleBottomY > fetchThrottleOffsetY {
viewModel.shouldFetch.send()
Self.scrollViewDidScrollToEnd(scrollView) {
viewModel.stateMachine.enter(FollowerListViewModel.State.Loading.self)
}
}
}

View File

@ -164,18 +164,7 @@ extension FollowingListViewController: DataSourceProvider {
extension FollowingListViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.isDragging || scrollView.isTracking { return }
let frame = scrollView.frame
let contentOffset = scrollView.contentOffset
let contentSize = scrollView.contentSize
let visibleBottomY = contentOffset.y + frame.height
let offset = 2 * frame.height
let fetchThrottleOffsetY = contentSize.height - offset
if visibleBottomY > fetchThrottleOffsetY {
Self.scrollViewDidScrollToEnd(scrollView) {
viewModel.shouldFetch.send()
}
}

View File

@ -54,17 +54,6 @@ extension UserTimelineViewController {
tableView: tableView,
statusTableViewCellDelegate: self
)
// setup batch fetch
viewModel.listBatchFetchViewModel.setup(scrollView: tableView)
viewModel.listBatchFetchViewModel.shouldFetch
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
guard let self = self else { return }
guard self.view.window != nil else { return }
self.viewModel.stateMachine.enter(UserTimelineViewModel.State.Loading.self)
}
.store(in: &disposeBag)
}
override func viewWillAppear(_ animated: Bool) {
@ -169,3 +158,12 @@ extension UserTimelineViewController: IndicatorInfoProvider {
return IndicatorInfo(title: viewModel.title)
}
}
//MARK: - UIScrollViewDelegate
extension UserTimelineViewController: UIScrollViewDelegate {
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
Self.scrollViewDidScrollToEnd(scrollView) {
viewModel.stateMachine.enter(UserTimelineViewModel.State.Loading.self)
}
}
}

View File

@ -22,7 +22,6 @@ final class UserTimelineViewModel {
let authContext: AuthContext
let title: String
let dataController: StatusDataController
let listBatchFetchViewModel = ListBatchFetchViewModel()
@Published var userIdentifier: UserIdentifier?
@Published var queryFilter: QueryFilter

View File

@ -49,18 +49,8 @@ extension FavoritedByViewController {
tableView: tableView,
userTableViewCellDelegate: self
)
// setup batch fetch
viewModel.listBatchFetchViewModel.setup(scrollView: tableView)
viewModel.listBatchFetchViewModel.shouldFetch
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
guard let self = self else { return }
self.viewModel.stateMachine.enter(UserListViewModel.State.Loading.self)
}
.store(in: &disposeBag)
viewModel.listBatchFetchViewModel.shouldFetch.send()
viewModel.stateMachine.enter(UserListViewModel.State.Loading.self)
}
override func viewWillAppear(_ animated: Bool) {
@ -91,3 +81,13 @@ extension FavoritedByViewController: UITableViewDelegate, AutoGenerateTableViewD
// MARK: - UserTableViewCellDelegate
extension FavoritedByViewController: UserTableViewCellDelegate {}
//MARK: - UIScrollViewDelegate
extension FavoritedByViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
Self.scrollViewDidScrollToEnd(scrollView) {
viewModel.stateMachine.enter(UserListViewModel.State.Loading.self)
}
}
}

View File

@ -56,17 +56,8 @@ extension RebloggedByViewController {
userTableViewCellDelegate: self
)
// setup batch fetch
viewModel.listBatchFetchViewModel.setup(scrollView: tableView)
viewModel.listBatchFetchViewModel.shouldFetch
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
guard let self = self else { return }
self.viewModel.stateMachine.enter(UserListViewModel.State.Loading.self)
}
.store(in: &disposeBag)
viewModel.listBatchFetchViewModel.shouldFetch.send()
viewModel.stateMachine.enter(UserListViewModel.State.Loading.self)
}
override func viewWillAppear(_ animated: Bool) {
@ -97,3 +88,13 @@ extension RebloggedByViewController: UITableViewDelegate, AutoGenerateTableViewD
// MARK: - UserTableViewCellDelegate
extension RebloggedByViewController: UserTableViewCellDelegate {}
//MARK: - UIScrollViewDelegate
extension RebloggedByViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
Self.scrollViewDidScrollToEnd(scrollView) {
viewModel.stateMachine.enter(UserListViewModel.State.Loading.self)
}
}
}

View File

@ -21,7 +21,6 @@ final class UserListViewModel {
let kind: Kind
@Published var accounts: [Mastodon.Entity.Account]
@Published var relationships: [Mastodon.Entity.Relationship]
let listBatchFetchViewModel = ListBatchFetchViewModel()
// output
var diffableDataSource: UITableViewDiffableDataSource<UserSection, UserItem>!

View File

@ -96,17 +96,6 @@ extension ReportStatusViewController {
}
.store(in: &observations)
// setup batch fetch
viewModel.listBatchFetchViewModel.setup(scrollView: tableView)
viewModel.listBatchFetchViewModel.shouldFetch
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
guard let self = self else { return }
guard self.view.window != nil else { return }
self.viewModel.stateMachine.enter(ReportStatusViewModel.State.Loading.self)
}
.store(in: &disposeBag)
viewModel.$isNextButtonEnabled
.receive(on: DispatchQueue.main)
.assign(to: \.isEnabled, on: navigationActionView.nextButton)
@ -119,7 +108,7 @@ extension ReportStatusViewController {
navigationActionView.backButton.addTarget(self, action: #selector(ReportStatusViewController.skipButtonDidPressed(_:)), for: .touchUpInside)
navigationActionView.nextButton.addTarget(self, action: #selector(ReportStatusViewController.nextButtonDidPressed(_:)), for: .touchUpInside)
viewModel.listBatchFetchViewModel.shouldFetch.send()
viewModel.stateMachine.enter(ReportStatusViewModel.State.Loading.self)
}
}
@ -197,3 +186,13 @@ extension ReportStatusViewController: UIAdaptivePresentationControllerDelegate {
return false
}
}
//MARK: - UIScrollViewDelegate
extension ReportStatusViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
Self.scrollViewDidScrollToEnd(scrollView) {
viewModel.stateMachine.enter(ReportStatusViewModel.State.Loading.self)
}
}
}

View File

@ -27,7 +27,6 @@ class ReportStatusViewModel {
let account: Mastodon.Entity.Account
let status: MastodonStatus?
let dataController: StatusDataController
let listBatchFetchViewModel = ListBatchFetchViewModel()
@Published var isSkip = false
@Published var selectStatuses = OrderedSet<MastodonStatus>()

View File

@ -50,25 +50,8 @@ extension SearchResultViewController {
userTableViewCellDelegate: self
)
// setup batch fetch
viewModel.listBatchFetchViewModel.setup(scrollView: tableView)
viewModel.listBatchFetchViewModel.shouldFetch
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
guard let self = self else { return }
guard self.view.window != nil else { return }
self.viewModel.stateMachine.enter(SearchResultViewModel.State.Loading.self)
}
.store(in: &disposeBag)
title = viewModel.searchText
viewModel.listBatchFetchViewModel.shouldFetch.send()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
viewModel.stateMachine.enter(SearchResultViewModel.State.Initial.self)
viewModel.stateMachine.enter(SearchResultViewModel.State.Loading.self)
}
}
@ -110,3 +93,13 @@ extension SearchResultViewController: StatusTableViewCellDelegate { }
// MARK: - UserTableViewCellDelegate
extension SearchResultViewController: UserTableViewCellDelegate {}
//MARK: - UIScrollViewDelegate
extension SearchResultViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
Self.scrollViewDidScrollToEnd(scrollView) {
viewModel.stateMachine.enter(SearchResultViewModel.State.Loading.self)
}
}
}

View File

@ -25,7 +25,6 @@ final class SearchResultViewModel {
@Published var accounts: [Mastodon.Entity.Account] = []
var relationships: [Mastodon.Entity.Relationship] = []
let dataController: StatusDataController
let listBatchFetchViewModel = ListBatchFetchViewModel()
var cellFrameCache = NSCache<NSNumber, NSValue>()
var navigationBarFrame = CurrentValueSubject<CGRect, Never>(.zero)

View File

@ -1,68 +0,0 @@
//
// ListBatchFetchViewModel.swift
// Mastodon
//
// Created by Cirno MainasuK on 2021-11-10.
//
import UIKit
import Combine
// ref: Texture.ASBatchFetchingDelegate
final class ListBatchFetchViewModel {
var disposeBag = Set<AnyCancellable>()
// timer running on `common` mode
let timerPublisher = Timer.publish(every: 30.0, on: .main, in: .common)
.autoconnect()
.share()
.eraseToAnyPublisher()
// input
private(set) weak var scrollView: UIScrollView?
let hasMore = CurrentValueSubject<Bool, Never>(true)
// output
let shouldFetch = PassthroughSubject<Void, Never>()
init() {
Publishers.CombineLatest(
hasMore,
timerPublisher
)
.sink { [weak self] hasMore, _ in
guard let self = self else { return }
guard hasMore else { return }
guard let scrollView = self.scrollView else { return }
// skip trigger if user interacting
if scrollView.isDragging || scrollView.isTracking { return }
// send fetch request
if scrollView.contentSize.height < scrollView.frame.height {
self.shouldFetch.send()
} else {
let frame = scrollView.frame
let contentOffset = scrollView.contentOffset
let contentSize = scrollView.contentSize
let visibleBottomY = contentOffset.y + frame.height
let offset = 2 * frame.height
let fetchThrottleOffsetY = contentSize.height - offset
if visibleBottomY > fetchThrottleOffsetY {
self.shouldFetch.send()
}
}
}
.store(in: &disposeBag)
}
}
extension ListBatchFetchViewModel {
func setup(scrollView: UIScrollView) {
self.scrollView = scrollView
}
}