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:
commit
ef8ceb56c9
@ -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 */,
|
||||
|
20
Mastodon/Extension/UIScrollViewDelegate.swift
Normal file
20
Mastodon/Extension/UIScrollViewDelegate.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,6 @@ final class DiscoveryNewsViewModel {
|
||||
// input
|
||||
let context: AppContext
|
||||
let authContext: AuthContext
|
||||
let listBatchFetchViewModel = ListBatchFetchViewModel()
|
||||
|
||||
// output
|
||||
@Published var links: [Mastodon.Entity.Link] = []
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>?
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>?
|
||||
|
@ -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
|
||||
|
@ -25,7 +25,6 @@ final class HomeTimelineViewModel: NSObject {
|
||||
let context: AppContext
|
||||
let authContext: AuthContext
|
||||
let dataController: FeedDataController
|
||||
let listBatchFetchViewModel = ListBatchFetchViewModel()
|
||||
|
||||
var presentedSuggestions = false
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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?
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,6 @@ final class BookmarkViewModel {
|
||||
let authContext: AuthContext
|
||||
|
||||
let dataController: StatusDataController
|
||||
let listBatchFetchViewModel = ListBatchFetchViewModel()
|
||||
|
||||
// output
|
||||
var diffableDataSource: UITableViewDiffableDataSource<StatusSection, StatusItem>?
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>?
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>!
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>()
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user