diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index d21f67b25..ba1946d0f 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -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 = ""; }; DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewController.swift; sourceTree = ""; }; DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewModel.swift; sourceTree = ""; }; - DB7274F3273BB9B200577D95 /* ListBatchFetchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListBatchFetchViewModel.swift; sourceTree = ""; }; + DB7274F3273BB9B200577D95 /* UIScrollViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIScrollViewDelegate.swift; sourceTree = ""; }; DB73B48F261F030A002E9E9F /* SafariActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariActivity.swift; sourceTree = ""; }; DB75BF1D263C1C1B00EDBF1F /* CustomScheduler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomScheduler.swift; sourceTree = ""; }; DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewController.swift; sourceTree = ""; }; @@ -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 = ""; @@ -2676,7 +2677,6 @@ DB9D6C2025E502C60051B173 /* ViewModel */ = { isa = PBXGroup; children = ( - DB7274F3273BB9B200577D95 /* ListBatchFetchViewModel.swift */, ); path = ViewModel; sourceTree = ""; @@ -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 */, diff --git a/Mastodon/Extension/UIScrollViewDelegate.swift b/Mastodon/Extension/UIScrollViewDelegate.swift new file mode 100644 index 000000000..7e289dee4 --- /dev/null +++ b/Mastodon/Extension/UIScrollViewDelegate.swift @@ -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() + } + } +} diff --git a/Mastodon/Scene/Discovery/News/DiscoveryNewsViewController.swift b/Mastodon/Scene/Discovery/News/DiscoveryNewsViewController.swift index 9f0e9bb03..bc09ad2f2 100644 --- a/Mastodon/Scene/Discovery/News/DiscoveryNewsViewController.swift +++ b/Mastodon/Scene/Discovery/News/DiscoveryNewsViewController.swift @@ -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) + } + } +} diff --git a/Mastodon/Scene/Discovery/News/DiscoveryNewsViewModel.swift b/Mastodon/Scene/Discovery/News/DiscoveryNewsViewModel.swift index 52061d5ae..5f5546945 100644 --- a/Mastodon/Scene/Discovery/News/DiscoveryNewsViewModel.swift +++ b/Mastodon/Scene/Discovery/News/DiscoveryNewsViewModel.swift @@ -20,7 +20,6 @@ final class DiscoveryNewsViewModel { // input let context: AppContext let authContext: AuthContext - let listBatchFetchViewModel = ListBatchFetchViewModel() // output @Published var links: [Mastodon.Entity.Link] = [] diff --git a/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewController.swift b/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewController.swift index ddf0997c0..3a2a96204 100644 --- a/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewController.swift +++ b/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewController.swift @@ -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) + } + } +} diff --git a/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel.swift b/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel.swift index 47f695987..3405c3456 100644 --- a/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel.swift +++ b/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel.swift @@ -21,7 +21,6 @@ final class DiscoveryPostsViewModel { let context: AppContext let authContext: AuthContext let dataController: StatusDataController - let listBatchFetchViewModel = ListBatchFetchViewModel() // output var diffableDataSource: UITableViewDiffableDataSource? diff --git a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift index 5d88e9b43..ab7755b3b 100644 --- a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift +++ b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift @@ -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) + } + } +} diff --git a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel.swift b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel.swift index 4635ee503..8e93f0817 100644 --- a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel.swift +++ b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel.swift @@ -28,7 +28,6 @@ final class HashtagTimelineViewModel { let isFetchingLatestTimeline = CurrentValueSubject(false) let timelinePredicate = CurrentValueSubject(nil) let hashtagEntity = CurrentValueSubject(nil) - let listBatchFetchViewModel = ListBatchFetchViewModel() // output var diffableDataSource: UITableViewDiffableDataSource? diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index ec08d0b19..6c19ad1ff 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -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 diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift index 4881f2f11..e6563f26a 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift @@ -25,7 +25,6 @@ final class HomeTimelineViewModel: NSObject { let context: AppContext let authContext: AuthContext let dataController: FeedDataController - let listBatchFetchViewModel = ListBatchFetchViewModel() var presentedSuggestions = false diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift index e5c29cbd6..cd8a3158e 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift @@ -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) + } + } +} diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift index c2ac144f8..0df34434a 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift @@ -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? diff --git a/Mastodon/Scene/Profile/Bookmark/BookmarkViewController.swift b/Mastodon/Scene/Profile/Bookmark/BookmarkViewController.swift index 47596e5ce..92b25a4df 100644 --- a/Mastodon/Scene/Profile/Bookmark/BookmarkViewController.swift +++ b/Mastodon/Scene/Profile/Bookmark/BookmarkViewController.swift @@ -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) + } + } +} diff --git a/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel.swift b/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel.swift index 420f0f94f..087dacfa0 100644 --- a/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel.swift +++ b/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel.swift @@ -21,7 +21,6 @@ final class BookmarkViewModel { let authContext: AuthContext let dataController: StatusDataController - let listBatchFetchViewModel = ListBatchFetchViewModel() // output var diffableDataSource: UITableViewDiffableDataSource? diff --git a/Mastodon/Scene/Profile/Favorite/FavoriteViewController.swift b/Mastodon/Scene/Profile/Favorite/FavoriteViewController.swift index 1c557516c..732281026 100644 --- a/Mastodon/Scene/Profile/Favorite/FavoriteViewController.swift +++ b/Mastodon/Scene/Profile/Favorite/FavoriteViewController.swift @@ -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) + } + } +} diff --git a/Mastodon/Scene/Profile/Favorite/FavoriteViewModel.swift b/Mastodon/Scene/Profile/Favorite/FavoriteViewModel.swift index 8e86b0256..6591c8312 100644 --- a/Mastodon/Scene/Profile/Favorite/FavoriteViewModel.swift +++ b/Mastodon/Scene/Profile/Favorite/FavoriteViewModel.swift @@ -20,7 +20,6 @@ final class FavoriteViewModel { let context: AppContext let authContext: AuthContext let dataController: StatusDataController - let listBatchFetchViewModel = ListBatchFetchViewModel() // output var diffableDataSource: UITableViewDiffableDataSource? diff --git a/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift b/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift index 19ff325dc..9eca1848b 100644 --- a/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift +++ b/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift @@ -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) } } } diff --git a/Mastodon/Scene/Profile/Following/FollowingListViewController.swift b/Mastodon/Scene/Profile/Following/FollowingListViewController.swift index 8db8178e5..e175a8920 100644 --- a/Mastodon/Scene/Profile/Following/FollowingListViewController.swift +++ b/Mastodon/Scene/Profile/Following/FollowingListViewController.swift @@ -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() } } diff --git a/Mastodon/Scene/Profile/Timeline/UserTimelineViewController.swift b/Mastodon/Scene/Profile/Timeline/UserTimelineViewController.swift index 71261ca52..bbbe4db4a 100644 --- a/Mastodon/Scene/Profile/Timeline/UserTimelineViewController.swift +++ b/Mastodon/Scene/Profile/Timeline/UserTimelineViewController.swift @@ -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) + } + } +} diff --git a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel.swift b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel.swift index 8598aafb9..bc59c592f 100644 --- a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel.swift +++ b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel.swift @@ -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 diff --git a/Mastodon/Scene/Profile/UserList/FavoritedBy/FavoritedByViewController.swift b/Mastodon/Scene/Profile/UserList/FavoritedBy/FavoritedByViewController.swift index b51ff70b9..4ac76ea09 100644 --- a/Mastodon/Scene/Profile/UserList/FavoritedBy/FavoritedByViewController.swift +++ b/Mastodon/Scene/Profile/UserList/FavoritedBy/FavoritedByViewController.swift @@ -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) + } + } +} diff --git a/Mastodon/Scene/Profile/UserList/RebloggedBy/RebloggedByViewController.swift b/Mastodon/Scene/Profile/UserList/RebloggedBy/RebloggedByViewController.swift index d50ade34b..30c0a72d6 100644 --- a/Mastodon/Scene/Profile/UserList/RebloggedBy/RebloggedByViewController.swift +++ b/Mastodon/Scene/Profile/UserList/RebloggedBy/RebloggedByViewController.swift @@ -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) + } + } +} diff --git a/Mastodon/Scene/Profile/UserList/UserListViewModel.swift b/Mastodon/Scene/Profile/UserList/UserListViewModel.swift index 14665334a..dc65bf4d1 100644 --- a/Mastodon/Scene/Profile/UserList/UserListViewModel.swift +++ b/Mastodon/Scene/Profile/UserList/UserListViewModel.swift @@ -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! diff --git a/Mastodon/Scene/Report/ReportStatus/ReportStatusViewController.swift b/Mastodon/Scene/Report/ReportStatus/ReportStatusViewController.swift index 9e51f8eb6..e928c59cf 100644 --- a/Mastodon/Scene/Report/ReportStatus/ReportStatusViewController.swift +++ b/Mastodon/Scene/Report/ReportStatus/ReportStatusViewController.swift @@ -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) + } + } +} diff --git a/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel.swift b/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel.swift index 030b81771..9e7b28e06 100644 --- a/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel.swift +++ b/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel.swift @@ -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() diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController.swift index 09448aeed..37a3e297e 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController.swift @@ -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) + } + } +} diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel.swift index 34b4f4299..6af3b0dcd 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel.swift @@ -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() var navigationBarFrame = CurrentValueSubject(.zero) diff --git a/Mastodon/Scene/Share/ViewModel/ListBatchFetchViewModel.swift b/Mastodon/Scene/Share/ViewModel/ListBatchFetchViewModel.swift deleted file mode 100644 index 4aae9d0a6..000000000 --- a/Mastodon/Scene/Share/ViewModel/ListBatchFetchViewModel.swift +++ /dev/null @@ -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() - - // 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(true) - - // output - let shouldFetch = PassthroughSubject() - - 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 - } -}