From 10f2358247e35e4e2eb8c4e6a2d07f4132dc36ba Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Wed, 25 Oct 2023 17:53:13 +0200 Subject: [PATCH 1/8] Migrate Followers-list to work with Mastodon.Entity.Account instead of MastodonUser --- Mastodon.xcodeproj/project.pbxproj | 6 +- .../xcshareddata/swiftpm/Package.resolved | 250 ------------------ ...istViewController+DataSourceProvider.swift | 34 --- .../Follower/FollowerListViewController.swift | 28 ++ .../FollowerListViewModel+Diffable.swift | 48 ++-- .../FollowerListViewModel+State.swift | 54 ++-- .../Follower/FollowerListViewModel.swift | 11 +- 7 files changed, 96 insertions(+), 335 deletions(-) delete mode 100644 Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved delete mode 100644 Mastodon/Scene/Profile/Follower/FollowerListViewController+DataSourceProvider.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 067137721..2ab5600b9 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -317,7 +317,7 @@ DB6180FA26391F2E0018D199 /* MediaPreviewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6180F926391F2E0018D199 /* MediaPreviewViewModel.swift */; }; DB63F7452799056400455B82 /* HashtagTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F7442799056400455B82 /* HashtagTableViewCell.swift */; }; DB63F74727990B0600455B82 /* DataSourceFacade+Hashtag.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F74627990B0600455B82 /* DataSourceFacade+Hashtag.swift */; }; - DB63F7492799126300455B82 /* FollowerListViewController+DataSourceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F7482799126300455B82 /* FollowerListViewController+DataSourceProvider.swift */; }; + DB63F74B279914A000455B82 /* FollowingListViewController+DataSourceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F74A279914A000455B82 /* FollowingListViewController+DataSourceProvider.swift */; }; DB63F74D27993F5B00455B82 /* SearchHistoryUserCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F74C27993F5B00455B82 /* SearchHistoryUserCollectionViewCell.swift */; }; DB63F74F2799405600455B82 /* SearchHistoryViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F74E2799405600455B82 /* SearchHistoryViewModel+Diffable.swift */; }; DB63F752279944AA00455B82 /* SearchHistorySectionHeaderCollectionReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F751279944AA00455B82 /* SearchHistorySectionHeaderCollectionReusableView.swift */; }; @@ -1021,7 +1021,7 @@ DB6180F926391F2E0018D199 /* MediaPreviewViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPreviewViewModel.swift; sourceTree = ""; }; DB63F7442799056400455B82 /* HashtagTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTableViewCell.swift; sourceTree = ""; }; DB63F74627990B0600455B82 /* DataSourceFacade+Hashtag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Hashtag.swift"; sourceTree = ""; }; - DB63F7482799126300455B82 /* FollowerListViewController+DataSourceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FollowerListViewController+DataSourceProvider.swift"; sourceTree = ""; }; + DB63F74A279914A000455B82 /* FollowingListViewController+DataSourceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FollowingListViewController+DataSourceProvider.swift"; sourceTree = ""; }; DB63F74C27993F5B00455B82 /* SearchHistoryUserCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistoryUserCollectionViewCell.swift; sourceTree = ""; }; DB63F74E2799405600455B82 /* SearchHistoryViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchHistoryViewModel+Diffable.swift"; sourceTree = ""; }; DB63F751279944AA00455B82 /* SearchHistorySectionHeaderCollectionReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistorySectionHeaderCollectionReusableView.swift; sourceTree = ""; }; @@ -2494,7 +2494,6 @@ isa = PBXGroup; children = ( DB6B74EE272FB55000C70B6E /* FollowerListViewController.swift */, - DB63F7482799126300455B82 /* FollowerListViewController+DataSourceProvider.swift */, DB6B74F1272FB67600C70B6E /* FollowerListViewModel.swift */, DB6B74F3272FBAE700C70B6E /* FollowerListViewModel+Diffable.swift */, DB6B74F5272FBCDB00C70B6E /* FollowerListViewModel+State.swift */, @@ -3738,7 +3737,6 @@ DBA5E7AB263BD3F5004598BB /* TimelineTableViewCellContextMenuConfiguration.swift in Sources */, DB73B490261F030A002E9E9F /* SafariActivity.swift in Sources */, 2AE244482927831100BDBF7C /* UIImage+SFSymbols.swift in Sources */, - DB63F7492799126300455B82 /* FollowerListViewController+DataSourceProvider.swift in Sources */, 2D198643261BF09500F0B013 /* SearchResultItem.swift in Sources */, 2DAC9E38262FC2320062E1A6 /* SuggestionAccountViewController.swift in Sources */, DB6180E02639194B0018D199 /* MediaPreviewPagingViewController.swift in Sources */, diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index ec05f44da..000000000 --- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,250 +0,0 @@ -{ - "object": { - "pins": [ - { - "package": "Alamofire", - "repositoryURL": "https://github.com/Alamofire/Alamofire.git", - "state": { - "branch": null, - "revision": "8dd85aee02e39dd280c75eef88ffdb86eed4b07b", - "version": "5.6.2" - } - }, - { - "package": "AlamofireImage", - "repositoryURL": "https://github.com/Alamofire/AlamofireImage.git", - "state": { - "branch": null, - "revision": "98cbb00ce0ec5fc8e52a5b50a6bfc08d3e5aee10", - "version": "4.2.0" - } - }, - { - "package": "FaviconFinder", - "repositoryURL": "https://github.com/will-lumley/FaviconFinder.git", - "state": { - "branch": null, - "revision": "1f74844f77f79b95c0bb0130b3a87d4f340e6d3a", - "version": "3.3.0" - } - }, - { - "package": "FLAnimatedImage", - "repositoryURL": "https://github.com/Flipboard/FLAnimatedImage.git", - "state": { - "branch": null, - "revision": "d4f07b6f164d53c1212c3e54d6460738b1981e9f", - "version": "1.0.17" - } - }, - { - "package": "Fuzi", - "repositoryURL": "https://github.com/cezheng/Fuzi.git", - "state": { - "branch": null, - "revision": "f08c8323da21e985f3772610753bcfc652c2103f", - "version": "3.1.3" - } - }, - { - "package": "FXPageControl", - "repositoryURL": "https://github.com/nicklockwood/FXPageControl.git", - "state": { - "branch": null, - "revision": "a94633402ba98c52f86c2a70e61ff086dec9de78", - "version": "1.6.0" - } - }, - { - "package": "Kanna", - "repositoryURL": "https://github.com/tid-kijyun/Kanna.git", - "state": { - "branch": null, - "revision": "f9e4922223dd0d3dfbf02ca70812cf5531fc0593", - "version": "5.2.7" - } - }, - { - "package": "KeychainAccess", - "repositoryURL": "https://github.com/kishikawakatsumi/KeychainAccess.git", - "state": { - "branch": null, - "revision": "84e546727d66f1adc5439debad16270d0fdd04e7", - "version": "4.2.2" - } - }, - { - "package": "LightChart", - "repositoryURL": "https://github.com/Bearologics/LightChart.git", - "state": { - "branch": "master", - "revision": "a7e724e9ec3cdcaa2d0840b95780e66b870dbf1e", - "version": null - } - }, - { - "package": "MBProgressHUD", - "repositoryURL": "https://github.com/jdg/MBProgressHUD.git", - "state": { - "branch": null, - "revision": "bca42b801100b2b3a4eda0ba8dd33d858c780b0d", - "version": "1.2.0" - } - }, - { - "package": "MetaTextKit", - "repositoryURL": "https://github.com/TwidereProject/MetaTextKit.git", - "state": { - "branch": null, - "revision": "dcd5255d6930c2fab408dc8562c577547e477624", - "version": "2.2.5" - } - }, - { - "package": "NextLevelSessionExporter", - "repositoryURL": "https://github.com/NextLevel/NextLevelSessionExporter.git", - "state": { - "branch": null, - "revision": "b6c0cce1aa37fe1547d694f958fac3c3524b74da", - "version": "0.4.6" - } - }, - { - "package": "Nuke", - "repositoryURL": "https://github.com/kean/Nuke.git", - "state": { - "branch": null, - "revision": "a002b7fd786f2df2ed4333fe73a9727499fd9d97", - "version": "10.11.2" - } - }, - { - "package": "Pageboy", - "repositoryURL": "https://github.com/uias/Pageboy", - "state": { - "branch": null, - "revision": "af8fa81788b893205e1ff42ddd88c5b0b315d7c5", - "version": "3.7.0" - } - }, - { - "package": "PanModal", - "repositoryURL": "https://github.com/slackhq/PanModal.git", - "state": { - "branch": null, - "revision": "b012aecb6b67a8e46369227f893c12544846613f", - "version": "1.2.7" - } - }, - { - "package": "SDWebImage", - "repositoryURL": "https://github.com/SDWebImage/SDWebImage.git", - "state": { - "branch": null, - "revision": "3312bf5e67b52fbce7c3caf431b0cda721a9f7bb", - "version": "5.14.2" - } - }, - { - "package": "Stripes", - "repositoryURL": "https://github.com/eneko/Stripes.git", - "state": { - "branch": null, - "revision": "d533fd44b8043a3abbf523e733599173d6f98c11", - "version": "0.2.0" - } - }, - { - "package": "swift-atomics", - "repositoryURL": "https://github.com/apple/swift-atomics.git", - "state": { - "branch": null, - "revision": "6c89474e62719ddcc1e9614989fff2f68208fe10", - "version": "1.1.0" - } - }, - { - "package": "swift-collections", - "repositoryURL": "https://github.com/apple/swift-collections.git", - "state": { - "branch": null, - "revision": "f504716c27d2e5d4144fa4794b12129301d17729", - "version": "1.0.3" - } - }, - { - "package": "swift-nio", - "repositoryURL": "https://github.com/apple/swift-nio.git", - "state": { - "branch": null, - "revision": "3db5c4aeee8100d2db6f1eaf3864afdad5dc68fd", - "version": "2.59.0" - } - }, - { - "package": "SwiftSoup", - "repositoryURL": "https://github.com/scinfu/SwiftSoup.git", - "state": { - "branch": null, - "revision": "6778575285177365cbad3e5b8a72f2a20583cfec", - "version": "2.4.3" - } - }, - { - "package": "TabBarPager", - "repositoryURL": "https://github.com/TwidereProject/TabBarPager.git", - "state": { - "branch": null, - "revision": "488aa66d157a648901b61721212c0dec23d27ee5", - "version": "0.1.0" - } - }, - { - "package": "Tabman", - "repositoryURL": "https://github.com/uias/Tabman", - "state": { - "branch": null, - "revision": "4a4f7c755b875ffd4f9ef10d67a67883669d2465", - "version": "2.13.0" - } - }, - { - "package": "TOCropViewController", - "repositoryURL": "https://github.com/TimOliver/TOCropViewController.git", - "state": { - "branch": null, - "revision": "d0470491f56e734731bbf77991944c0dfdee3e0e", - "version": "2.6.1" - } - }, - { - "package": "UIHostingConfigurationBackport", - "repositoryURL": "https://github.com/woxtu/UIHostingConfigurationBackport.git", - "state": { - "branch": null, - "revision": "6091f2d38faa4b24fc2ca0389c651e2f666624a3", - "version": "0.1.0" - } - }, - { - "package": "UITextView+Placeholder", - "repositoryURL": "https://github.com/MainasuK/UITextView-Placeholder.git", - "state": { - "branch": null, - "revision": "20f513ded04a040cdf5467f0891849b1763ede3b", - "version": "1.4.1" - } - }, - { - "package": "XLPagerTabStrip", - "repositoryURL": "https://github.com/xmartlabs/XLPagerTabStrip.git", - "state": { - "branch": null, - "revision": "211ed62aa376722cf93c429802a8b6ff66a8bd52", - "version": "9.1.0" - } - } - ] - }, - "version": 1 -} diff --git a/Mastodon/Scene/Profile/Follower/FollowerListViewController+DataSourceProvider.swift b/Mastodon/Scene/Profile/Follower/FollowerListViewController+DataSourceProvider.swift deleted file mode 100644 index 956cb0704..000000000 --- a/Mastodon/Scene/Profile/Follower/FollowerListViewController+DataSourceProvider.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// FollowerListViewController+DataSourceProvider.swift -// Mastodon -// -// Created by MainasuK on 2022-1-20. -// - -import UIKit - -extension FollowerListViewController: DataSourceProvider { - func item(from source: DataSourceItem.Source) async -> DataSourceItem? { - var _indexPath = source.indexPath - if _indexPath == nil, let cell = source.tableViewCell { - _indexPath = await self.indexPath(for: cell) - } - guard let indexPath = _indexPath else { return nil } - - guard let item = viewModel.diffableDataSource?.itemIdentifier(for: indexPath) else { - return nil - } - - switch item { - case .user(let record): - return .user(record: record) - default: - return nil - } - } - - @MainActor - private func indexPath(for cell: UITableViewCell) async -> IndexPath? { - return tableView.indexPath(for: cell) - } -} diff --git a/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift b/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift index 733bd1699..d438a0ec0 100644 --- a/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift +++ b/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift @@ -107,3 +107,31 @@ extension FollowerListViewController: UITableViewDelegate, AutoGenerateTableView // MARK: - UserTableViewCellDelegate extension FollowerListViewController: UserTableViewCellDelegate {} + + +// MARK: - DataSourceProvider +extension FollowerListViewController: DataSourceProvider { + func item(from source: DataSourceItem.Source) async -> DataSourceItem? { + var _indexPath = source.indexPath + if _indexPath == nil, let cell = source.tableViewCell { + _indexPath = await self.indexPath(for: cell) + } + guard let indexPath = _indexPath else { return nil } + + guard let item = viewModel.diffableDataSource?.itemIdentifier(for: indexPath) else { + return nil + } + + switch item { + case .account(let account, let relationship): + return .account(account: account, relationship: relationship) + default: + return nil + } + } + + @MainActor + private func indexPath(for cell: UITableViewCell) async -> IndexPath? { + return tableView.indexPath(for: cell) + } +} diff --git a/Mastodon/Scene/Profile/Follower/FollowerListViewModel+Diffable.swift b/Mastodon/Scene/Profile/Follower/FollowerListViewModel+Diffable.swift index d0676dc59..d3384e538 100644 --- a/Mastodon/Scene/Profile/Follower/FollowerListViewModel+Diffable.swift +++ b/Mastodon/Scene/Profile/Follower/FollowerListViewModel+Diffable.swift @@ -8,6 +8,7 @@ import UIKit import MastodonAsset import MastodonLocalization +import MastodonSDK extension FollowerListViewModel { func setupDiffableDataSource( @@ -27,34 +28,43 @@ extension FollowerListViewModel { snapshot.appendSections([.main]) snapshot.appendItems([.bottomLoader], toSection: .main) diffableDataSource?.applySnapshotUsingReloadData(snapshot, completion: nil) - - userFetchedResultsController.$records + + $accounts .receive(on: DispatchQueue.main) - .sink { [weak self] records in - guard let self = self else { return } + .sink { [weak self] accounts in + guard let self else { return } guard let diffableDataSource = self.diffableDataSource else { return } - + var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.main]) - let items = records.map { UserItem.user(record: $0) } + + let accountsWithRelationship: [(account: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?)] = accounts.compactMap { account in + guard let relationship = self.relationships.first(where: {$0.id == account.id }) else { return (account: account, relationship: nil)} + + return (account: account, relationship: relationship) + } + + let items = accountsWithRelationship.map { UserItem.account(account: $0.account, relationship: $0.relationship) } snapshot.appendItems(items, toSection: .main) - + if let currentState = self.stateMachine.currentState { switch currentState { - case is State.Idle, is State.Loading, is State.Fail: - snapshot.appendItems([.bottomLoader], toSection: .main) - case is State.NoMore: - guard let userID = self.userID, - userID != self.authContext.mastodonAuthenticationBox.userID - else { break } - // display hint footer exclude self - let text = L10n.Scene.Follower.footer - snapshot.appendItems([.bottomHeader(text: text)], toSection: .main) - default: - break + case is State.Loading: + snapshot.appendItems([.bottomLoader], toSection: .main) + case is State.NoMore: + guard let userID = self.userID, + userID != self.authContext.mastodonAuthenticationBox.userID + else { break } + // display footer exclude self + let text = L10n.Scene.Following.footer + snapshot.appendItems([.bottomHeader(text: text)], toSection: .main) + case is State.Idle, is State.Fail: + break + default: + break } } - + diffableDataSource.apply(snapshot, animatingDifferences: false) } .store(in: &disposeBag) diff --git a/Mastodon/Scene/Profile/Follower/FollowerListViewModel+State.swift b/Mastodon/Scene/Profile/Follower/FollowerListViewModel+State.swift index fb7b20166..51c189dd8 100644 --- a/Mastodon/Scene/Profile/Follower/FollowerListViewModel+State.swift +++ b/Mastodon/Scene/Profile/Follower/FollowerListViewModel+State.swift @@ -61,8 +61,9 @@ extension FollowerListViewModel.State { guard let viewModel = viewModel, let stateMachine = stateMachine else { return } // reset - viewModel.userFetchedResultsController.userIDs = [] - + viewModel.accounts = [] + viewModel.relationships = [] + stateMachine.enter(Loading.self) } } @@ -123,47 +124,58 @@ extension FollowerListViewModel.State { maxID = nil } - guard let viewModel = viewModel, let stateMachine = stateMachine else { return } + guard let viewModel, let stateMachine else { return } - guard let userID = viewModel.userID, !userID.isEmpty else { + guard let userID = viewModel.userID, userID.isEmpty == false else { stateMachine.enter(Fail.self) return } - + Task { do { - let response = try await viewModel.context.apiService.followers( + let accountResponse = try await viewModel.context.apiService.followers( userID: userID, maxID: maxID, authenticationBox: viewModel.authContext.mastodonAuthenticationBox ) - + var hasNewAppend = false - var userIDs = viewModel.userFetchedResultsController.userIDs - for user in response.value { - guard !userIDs.contains(user.id) else { continue } - userIDs.append(user.id) + + let newRelationships = try await viewModel.context.apiService.relationship(forAccounts: accountResponse.value, authenticationBox: viewModel.authContext.mastodonAuthenticationBox) + + var accounts = viewModel.accounts + + for user in accountResponse.value { + guard accounts.contains(user) == false else { continue } + accounts.append(user) hasNewAppend = true } - - let maxID = response.link?.maxID - - if hasNewAppend && maxID != nil { + + var relationships = viewModel.relationships + + for relationship in newRelationships.value { + guard relationships.contains(relationship) == false else { continue } + relationships.append(relationship) + } + + let maxID = accountResponse.link?.maxID + + if hasNewAppend, maxID != nil { await enter(state: Idle.self) } else { await enter(state: NoMore.self) } - + + viewModel.accounts = accounts + viewModel.relationships = relationships self.maxID = maxID - viewModel.userFetchedResultsController.userIDs = userIDs - } catch { await enter(state: Fail.self) } - } // end Task - } // end func didEnter + } + } } - + class NoMore: FollowerListViewModel.State { override func isValidNextState(_ stateClass: AnyClass) -> Bool { switch stateClass { diff --git a/Mastodon/Scene/Profile/Follower/FollowerListViewModel.swift b/Mastodon/Scene/Profile/Follower/FollowerListViewModel.swift index ab1144e54..81441da79 100644 --- a/Mastodon/Scene/Profile/Follower/FollowerListViewModel.swift +++ b/Mastodon/Scene/Profile/Follower/FollowerListViewModel.swift @@ -19,7 +19,8 @@ final class FollowerListViewModel { // input let context: AppContext let authContext: AuthContext - let userFetchedResultsController: UserFetchedResultsController + @Published var accounts: [Mastodon.Entity.Account] + @Published var relationships: [Mastodon.Entity.Relationship] let listBatchFetchViewModel = ListBatchFetchViewModel() @Published var domain: String? @@ -48,13 +49,9 @@ final class FollowerListViewModel { ) { self.context = context self.authContext = authContext - self.userFetchedResultsController = UserFetchedResultsController( - managedObjectContext: context.managedObjectContext, - domain: domain, - additionalPredicate: nil - ) self.domain = domain self.userID = userID - // end init + self.accounts = [] + self.relationships = [] } } From 96bea2ad95abfb8e216ef8aa6fbabc1eb91912bf Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Wed, 25 Oct 2023 18:03:46 +0200 Subject: [PATCH 2/8] Add pull to refresh to Followers-list --- Mastodon/Coordinator/SceneCoordinator.swift | 5 +- .../Follower/FollowerListViewController.swift | 53 +++++++++++++++---- .../FollowerListViewModel+State.swift | 9 +++- .../Follower/FollowerListViewModel.swift | 8 ++- 4 files changed, 58 insertions(+), 17 deletions(-) diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index a12d3509f..3fb91cad6 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -464,9 +464,8 @@ private extension SceneCoordinator { _viewController.viewModel = viewModel viewController = _viewController case .follower(let viewModel): - let _viewController = FollowerListViewController() - _viewController.viewModel = viewModel - viewController = _viewController + let followerListViewController = FollowerListViewController(viewModel: viewModel, coordinator: self, context: appContext) + viewController = followerListViewController case .following(let viewModel): let followingListViewController = FollowingListViewController(viewModel: viewModel, coordinator: self, context: appContext) viewController = followingListViewController diff --git a/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift b/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift index d438a0ec0..2753f177a 100644 --- a/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift +++ b/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift @@ -11,28 +11,53 @@ import Combine import MastodonCore import MastodonUI import MastodonLocalization -import CoreDataStack final class FollowerListViewController: UIViewController, NeedsDependency { - weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } - weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } + weak var context: AppContext! + weak var coordinator: SceneCoordinator! var disposeBag = Set() - var viewModel: FollowerListViewModel! + var viewModel: FollowerListViewModel - lazy var tableView: UITableView = { - let tableView = UITableView() + let tableView: UITableView + let refreshControl: UIRefreshControl + + init(viewModel: FollowerListViewModel, coordinator: SceneCoordinator, context: AppContext) { + + self.context = context + self.coordinator = coordinator + self.viewModel = viewModel + + tableView = UITableView() + tableView.translatesAutoresizingMaskIntoConstraints = false tableView.register(UserTableViewCell.self, forCellReuseIdentifier: String(describing: UserTableViewCell.self)) tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self)) tableView.register(TimelineFooterTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineFooterTableViewCell.self)) tableView.rowHeight = UITableView.automaticDimension tableView.separatorStyle = .none tableView.backgroundColor = .clear - return tableView - }() - - + + refreshControl = UIRefreshControl() + tableView.refreshControl = refreshControl + + super.init(nibName: nil, bundle: nil) + + title = L10n.Scene.Following.title + + view.backgroundColor = .secondarySystemBackground + + view.addSubview(tableView) + tableView.pinToParent() + tableView.delegate = self + tableView.refreshControl?.addTarget(self, action: #selector(FollowingListViewController.refresh(_:)), for: .valueChanged) + + viewModel.tableView = tableView + + refreshControl.addTarget(self, action: #selector(FollowerListViewController.refresh(_:)), for: .valueChanged) + } + + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } extension FollowerListViewController { @@ -82,7 +107,13 @@ extension FollowerListViewController { tableView.deselectRow(with: transitionCoordinator, animated: animated) } - + + //MARK: - Actions + + @objc + func refresh(_ sender: UIRefreshControl) { + viewModel.stateMachine.enter(FollowerListViewModel.State.Reloading.self) + } } // MARK: - AuthContextProvider diff --git a/Mastodon/Scene/Profile/Follower/FollowerListViewModel+State.swift b/Mastodon/Scene/Profile/Follower/FollowerListViewModel+State.swift index 51c189dd8..eeb285917 100644 --- a/Mastodon/Scene/Profile/Follower/FollowerListViewModel+State.swift +++ b/Mastodon/Scene/Profile/Follower/FollowerListViewModel+State.swift @@ -9,7 +9,6 @@ import Foundation import GameplayKit import MastodonSDK import MastodonCore -import CoreDataStack extension FollowerListViewModel { class State: GKState { @@ -98,6 +97,12 @@ extension FollowerListViewModel.State { return false } } + + override func didEnter(from previousState: GKState?) { + super.didEnter(from: previousState) + + viewModel?.tableView?.refreshControl?.endRefreshing() + } } class Loading: FollowerListViewModel.State { @@ -188,6 +193,8 @@ extension FollowerListViewModel.State { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) + + viewModel?.tableView?.refreshControl?.endRefreshing() } } } diff --git a/Mastodon/Scene/Profile/Follower/FollowerListViewModel.swift b/Mastodon/Scene/Profile/Follower/FollowerListViewModel.swift index 81441da79..df0751a3c 100644 --- a/Mastodon/Scene/Profile/Follower/FollowerListViewModel.swift +++ b/Mastodon/Scene/Profile/Follower/FollowerListViewModel.swift @@ -21,11 +21,14 @@ final class FollowerListViewModel { let authContext: AuthContext @Published var accounts: [Mastodon.Entity.Account] @Published var relationships: [Mastodon.Entity.Relationship] - let listBatchFetchViewModel = ListBatchFetchViewModel() + + let listBatchFetchViewModel: ListBatchFetchViewModel @Published var domain: String? @Published var userID: String? - + + var tableView: UITableView? + // output var diffableDataSource: UITableViewDiffableDataSource? private(set) lazy var stateMachine: GKStateMachine = { @@ -53,5 +56,6 @@ final class FollowerListViewModel { self.userID = userID self.accounts = [] self.relationships = [] + self.listBatchFetchViewModel = ListBatchFetchViewModel() } } From aedade6d5b2eb41e2f06c118dfc1e8eb321293f3 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Wed, 25 Oct 2023 18:22:09 +0200 Subject: [PATCH 3/8] Fix Merge-fuckups --- Mastodon.xcodeproj/project.pbxproj | 2 - .../xcshareddata/swiftpm/Package.resolved | 250 ++++++++++++++++++ 2 files changed, 250 insertions(+), 2 deletions(-) create mode 100644 Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 2ab5600b9..555c9747e 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -317,7 +317,6 @@ DB6180FA26391F2E0018D199 /* MediaPreviewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6180F926391F2E0018D199 /* MediaPreviewViewModel.swift */; }; DB63F7452799056400455B82 /* HashtagTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F7442799056400455B82 /* HashtagTableViewCell.swift */; }; DB63F74727990B0600455B82 /* DataSourceFacade+Hashtag.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F74627990B0600455B82 /* DataSourceFacade+Hashtag.swift */; }; - DB63F74B279914A000455B82 /* FollowingListViewController+DataSourceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F74A279914A000455B82 /* FollowingListViewController+DataSourceProvider.swift */; }; DB63F74D27993F5B00455B82 /* SearchHistoryUserCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F74C27993F5B00455B82 /* SearchHistoryUserCollectionViewCell.swift */; }; DB63F74F2799405600455B82 /* SearchHistoryViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F74E2799405600455B82 /* SearchHistoryViewModel+Diffable.swift */; }; DB63F752279944AA00455B82 /* SearchHistorySectionHeaderCollectionReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F751279944AA00455B82 /* SearchHistorySectionHeaderCollectionReusableView.swift */; }; @@ -1021,7 +1020,6 @@ DB6180F926391F2E0018D199 /* MediaPreviewViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPreviewViewModel.swift; sourceTree = ""; }; DB63F7442799056400455B82 /* HashtagTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTableViewCell.swift; sourceTree = ""; }; DB63F74627990B0600455B82 /* DataSourceFacade+Hashtag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Hashtag.swift"; sourceTree = ""; }; - DB63F74A279914A000455B82 /* FollowingListViewController+DataSourceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FollowingListViewController+DataSourceProvider.swift"; sourceTree = ""; }; DB63F74C27993F5B00455B82 /* SearchHistoryUserCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistoryUserCollectionViewCell.swift; sourceTree = ""; }; DB63F74E2799405600455B82 /* SearchHistoryViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchHistoryViewModel+Diffable.swift"; sourceTree = ""; }; DB63F751279944AA00455B82 /* SearchHistorySectionHeaderCollectionReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistorySectionHeaderCollectionReusableView.swift; sourceTree = ""; }; diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 000000000..ec05f44da --- /dev/null +++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,250 @@ +{ + "object": { + "pins": [ + { + "package": "Alamofire", + "repositoryURL": "https://github.com/Alamofire/Alamofire.git", + "state": { + "branch": null, + "revision": "8dd85aee02e39dd280c75eef88ffdb86eed4b07b", + "version": "5.6.2" + } + }, + { + "package": "AlamofireImage", + "repositoryURL": "https://github.com/Alamofire/AlamofireImage.git", + "state": { + "branch": null, + "revision": "98cbb00ce0ec5fc8e52a5b50a6bfc08d3e5aee10", + "version": "4.2.0" + } + }, + { + "package": "FaviconFinder", + "repositoryURL": "https://github.com/will-lumley/FaviconFinder.git", + "state": { + "branch": null, + "revision": "1f74844f77f79b95c0bb0130b3a87d4f340e6d3a", + "version": "3.3.0" + } + }, + { + "package": "FLAnimatedImage", + "repositoryURL": "https://github.com/Flipboard/FLAnimatedImage.git", + "state": { + "branch": null, + "revision": "d4f07b6f164d53c1212c3e54d6460738b1981e9f", + "version": "1.0.17" + } + }, + { + "package": "Fuzi", + "repositoryURL": "https://github.com/cezheng/Fuzi.git", + "state": { + "branch": null, + "revision": "f08c8323da21e985f3772610753bcfc652c2103f", + "version": "3.1.3" + } + }, + { + "package": "FXPageControl", + "repositoryURL": "https://github.com/nicklockwood/FXPageControl.git", + "state": { + "branch": null, + "revision": "a94633402ba98c52f86c2a70e61ff086dec9de78", + "version": "1.6.0" + } + }, + { + "package": "Kanna", + "repositoryURL": "https://github.com/tid-kijyun/Kanna.git", + "state": { + "branch": null, + "revision": "f9e4922223dd0d3dfbf02ca70812cf5531fc0593", + "version": "5.2.7" + } + }, + { + "package": "KeychainAccess", + "repositoryURL": "https://github.com/kishikawakatsumi/KeychainAccess.git", + "state": { + "branch": null, + "revision": "84e546727d66f1adc5439debad16270d0fdd04e7", + "version": "4.2.2" + } + }, + { + "package": "LightChart", + "repositoryURL": "https://github.com/Bearologics/LightChart.git", + "state": { + "branch": "master", + "revision": "a7e724e9ec3cdcaa2d0840b95780e66b870dbf1e", + "version": null + } + }, + { + "package": "MBProgressHUD", + "repositoryURL": "https://github.com/jdg/MBProgressHUD.git", + "state": { + "branch": null, + "revision": "bca42b801100b2b3a4eda0ba8dd33d858c780b0d", + "version": "1.2.0" + } + }, + { + "package": "MetaTextKit", + "repositoryURL": "https://github.com/TwidereProject/MetaTextKit.git", + "state": { + "branch": null, + "revision": "dcd5255d6930c2fab408dc8562c577547e477624", + "version": "2.2.5" + } + }, + { + "package": "NextLevelSessionExporter", + "repositoryURL": "https://github.com/NextLevel/NextLevelSessionExporter.git", + "state": { + "branch": null, + "revision": "b6c0cce1aa37fe1547d694f958fac3c3524b74da", + "version": "0.4.6" + } + }, + { + "package": "Nuke", + "repositoryURL": "https://github.com/kean/Nuke.git", + "state": { + "branch": null, + "revision": "a002b7fd786f2df2ed4333fe73a9727499fd9d97", + "version": "10.11.2" + } + }, + { + "package": "Pageboy", + "repositoryURL": "https://github.com/uias/Pageboy", + "state": { + "branch": null, + "revision": "af8fa81788b893205e1ff42ddd88c5b0b315d7c5", + "version": "3.7.0" + } + }, + { + "package": "PanModal", + "repositoryURL": "https://github.com/slackhq/PanModal.git", + "state": { + "branch": null, + "revision": "b012aecb6b67a8e46369227f893c12544846613f", + "version": "1.2.7" + } + }, + { + "package": "SDWebImage", + "repositoryURL": "https://github.com/SDWebImage/SDWebImage.git", + "state": { + "branch": null, + "revision": "3312bf5e67b52fbce7c3caf431b0cda721a9f7bb", + "version": "5.14.2" + } + }, + { + "package": "Stripes", + "repositoryURL": "https://github.com/eneko/Stripes.git", + "state": { + "branch": null, + "revision": "d533fd44b8043a3abbf523e733599173d6f98c11", + "version": "0.2.0" + } + }, + { + "package": "swift-atomics", + "repositoryURL": "https://github.com/apple/swift-atomics.git", + "state": { + "branch": null, + "revision": "6c89474e62719ddcc1e9614989fff2f68208fe10", + "version": "1.1.0" + } + }, + { + "package": "swift-collections", + "repositoryURL": "https://github.com/apple/swift-collections.git", + "state": { + "branch": null, + "revision": "f504716c27d2e5d4144fa4794b12129301d17729", + "version": "1.0.3" + } + }, + { + "package": "swift-nio", + "repositoryURL": "https://github.com/apple/swift-nio.git", + "state": { + "branch": null, + "revision": "3db5c4aeee8100d2db6f1eaf3864afdad5dc68fd", + "version": "2.59.0" + } + }, + { + "package": "SwiftSoup", + "repositoryURL": "https://github.com/scinfu/SwiftSoup.git", + "state": { + "branch": null, + "revision": "6778575285177365cbad3e5b8a72f2a20583cfec", + "version": "2.4.3" + } + }, + { + "package": "TabBarPager", + "repositoryURL": "https://github.com/TwidereProject/TabBarPager.git", + "state": { + "branch": null, + "revision": "488aa66d157a648901b61721212c0dec23d27ee5", + "version": "0.1.0" + } + }, + { + "package": "Tabman", + "repositoryURL": "https://github.com/uias/Tabman", + "state": { + "branch": null, + "revision": "4a4f7c755b875ffd4f9ef10d67a67883669d2465", + "version": "2.13.0" + } + }, + { + "package": "TOCropViewController", + "repositoryURL": "https://github.com/TimOliver/TOCropViewController.git", + "state": { + "branch": null, + "revision": "d0470491f56e734731bbf77991944c0dfdee3e0e", + "version": "2.6.1" + } + }, + { + "package": "UIHostingConfigurationBackport", + "repositoryURL": "https://github.com/woxtu/UIHostingConfigurationBackport.git", + "state": { + "branch": null, + "revision": "6091f2d38faa4b24fc2ca0389c651e2f666624a3", + "version": "0.1.0" + } + }, + { + "package": "UITextView+Placeholder", + "repositoryURL": "https://github.com/MainasuK/UITextView-Placeholder.git", + "state": { + "branch": null, + "revision": "20f513ded04a040cdf5467f0891849b1763ede3b", + "version": "1.4.1" + } + }, + { + "package": "XLPagerTabStrip", + "repositoryURL": "https://github.com/xmartlabs/XLPagerTabStrip.git", + "state": { + "branch": null, + "revision": "211ed62aa376722cf93c429802a8b6ff66a8bd52", + "version": "9.1.0" + } + } + ] + }, + "version": 1 +} From 0c3313227b6d2d28f317db5ac769d43cdb47c764 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Sun, 5 Nov 2023 21:19:53 +0100 Subject: [PATCH 4/8] Fix warning --- Mastodon/Coordinator/SceneCoordinator.swift | 62 +++++++++------------ 1 file changed, 27 insertions(+), 35 deletions(-) diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index 3fb91cad6..d98e27b25 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -222,43 +222,35 @@ extension SceneCoordinator { func setup() { let rootViewController: UIViewController - - do { - let _authentication = AuthenticationServiceProvider.shared.authenticationSortedByActivation().first - let _authContext = _authentication.flatMap { AuthContext(authentication: $0) } - self.authContext = _authContext - - switch UIDevice.current.userInterfaceIdiom { - case .phone: - let viewController = MainTabBarController(context: appContext, coordinator: self, authContext: _authContext) - self.splitViewController = nil - self.tabBarController = viewController - rootViewController = viewController - default: - let splitViewController = RootSplitViewController(context: appContext, coordinator: self, authContext: _authContext) - self.splitViewController = splitViewController - self.tabBarController = splitViewController.contentSplitViewController.mainTabBarController - rootViewController = splitViewController - } - sceneDelegate.window?.rootViewController = rootViewController // base: main - self.rootViewController = rootViewController - if _authContext == nil { // entry #1: welcome - DispatchQueue.main.async { - _ = self.present( - scene: .welcome, - from: self.sceneDelegate.window?.rootViewController, - transition: .modal(animated: true, completion: nil) - ) - } + let _authentication = AuthenticationServiceProvider.shared.authenticationSortedByActivation().first + let _authContext = _authentication.flatMap { AuthContext(authentication: $0) } + self.authContext = _authContext + + switch UIDevice.current.userInterfaceIdiom { + case .phone: + let viewController = MainTabBarController(context: appContext, coordinator: self, authContext: _authContext) + self.splitViewController = nil + self.tabBarController = viewController + rootViewController = viewController + default: + let splitViewController = RootSplitViewController(context: appContext, coordinator: self, authContext: _authContext) + self.splitViewController = splitViewController + self.tabBarController = splitViewController.contentSplitViewController.mainTabBarController + rootViewController = splitViewController + } + + sceneDelegate.window?.rootViewController = rootViewController // base: main + self.rootViewController = rootViewController + + if _authContext == nil { // entry #1: welcome + DispatchQueue.main.async { + _ = self.present( + scene: .welcome, + from: self.sceneDelegate.window?.rootViewController, + transition: .modal(animated: true, completion: nil) + ) } - - } catch { - assertionFailure(error.localizedDescription) - Task { - try? await Task.sleep(nanoseconds: .second * 2) - setup() // entry #2: retry - } // end Task } } From 32e656f342e0f3b16d5d436f285ede727336f134 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Sun, 5 Nov 2023 21:31:12 +0100 Subject: [PATCH 5/8] Fix more warnings --- .../Register/MastodonRegisterViewController.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift index c327357bc..864b0355e 100644 --- a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift +++ b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift @@ -150,7 +150,7 @@ extension MastodonRegisterViewController { return "en" } let fallbackLanguageCode: String = { - let code = Locale.current.languageCode ?? "en" + let code = Locale.current.language.languageCode?.identifier ?? "en" guard localCode[code] != nil else { return "en" } return code }() @@ -161,7 +161,7 @@ extension MastodonRegisterViewController { } // prepare languageCode and validate then return fallback if needs let local = Locale(identifier: identifier) - guard let languageCode = local.languageCode, + guard let languageCode = local.language.languageCode?.identifier, localCode[languageCode] != nil else { return fallbackLanguageCode @@ -170,10 +170,10 @@ extension MastodonRegisterViewController { let extendCodes: [String] = { let locales = Locale.preferredLanguages.map { Locale(identifier: $0) } return locales.compactMap { locale in - guard let languageCode = locale.languageCode, - let regionCode = locale.regionCode + guard let languageCode = locale.language.languageCode?.identifier, + let regionIdentifier = locale.region?.identifier else { return nil } - return languageCode + "-" + regionCode + return languageCode + "-" + regionIdentifier } }() let _firstMatchExtendCode = extendCodes.first { code in From 83e45847fe350d29c230af5fbd578a9650060937 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Sun, 5 Nov 2023 21:34:55 +0100 Subject: [PATCH 6/8] Fix another warning --- .../Protocol/Provider/DataSourceFacade+Translate.swift | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Translate.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Translate.swift index 912540f69..7560d9008 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Translate.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Translate.swift @@ -49,11 +49,12 @@ private extension DataSourceFacade { authenticationBox: provider.authContext.mastodonAuthenticationBox ).value - guard let content = value.content else { + if value.content != nil { + return value + } else { return nil } - - return value + } catch { throw TranslationFailure.emptyOrInvalidResponse } From f3a7fe83207762ed87abf1fa7284b1e1e806a3e4 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Thu, 9 Nov 2023 11:52:31 +0100 Subject: [PATCH 7/8] Fix title --- .../Scene/Profile/Follower/FollowerListViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift b/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift index 2753f177a..2591caf87 100644 --- a/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift +++ b/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift @@ -43,7 +43,7 @@ final class FollowerListViewController: UIViewController, NeedsDependency { super.init(nibName: nil, bundle: nil) - title = L10n.Scene.Following.title + title = L10n.Scene.Follower.title view.backgroundColor = .secondarySystemBackground From 48d7592a9a18ca3567d229183f166541f69018e5 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Thu, 9 Nov 2023 12:11:14 +0100 Subject: [PATCH 8/8] Don't run into an endless loop if no followers exist See also 3fc2793 --- .../Follower/FollowerListViewController.swift | 37 +++++++++++++------ .../FollowerListViewModel+State.swift | 8 ++++ .../Follower/FollowerListViewModel.swift | 7 ++-- .../FollowingListViewController.swift | 1 - 4 files changed, 36 insertions(+), 17 deletions(-) diff --git a/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift b/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift index 2591caf87..daa2838a5 100644 --- a/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift +++ b/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift @@ -50,7 +50,7 @@ final class FollowerListViewController: UIViewController, NeedsDependency { view.addSubview(tableView) tableView.pinToParent() tableView.delegate = self - tableView.refreshControl?.addTarget(self, action: #selector(FollowingListViewController.refresh(_:)), for: .valueChanged) + tableView.refreshControl?.addTarget(self, action: #selector(FollowerListViewController.refresh(_:)), for: .valueChanged) viewModel.tableView = tableView @@ -65,23 +65,13 @@ extension FollowerListViewController { override func viewDidLoad() { super.viewDidLoad() - title = L10n.Scene.Follower.title - - view.backgroundColor = .secondarySystemBackground - - tableView.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(tableView) - tableView.pinToParent() - - tableView.delegate = self viewModel.setupDiffableDataSource( tableView: tableView, userTableViewCellDelegate: self ) // setup batch fetch - viewModel.listBatchFetchViewModel.setup(scrollView: tableView) - viewModel.listBatchFetchViewModel.shouldFetch + viewModel.shouldFetch .receive(on: DispatchQueue.main) .sink { [weak self] _ in guard let self = self else { return } @@ -100,6 +90,8 @@ extension FollowerListViewController { self.viewModel.stateMachine.enter(FollowerListViewModel.State.Reloading.self) } .store(in: &disposeBag) + + tableView.refreshControl = UIRefreshControl() } override func viewWillAppear(_ animated: Bool) { @@ -166,3 +158,24 @@ extension FollowerListViewController: DataSourceProvider { return tableView.indexPath(for: cell) } } + +//MARK: - UIScrollViewDelegate + +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() + } + } +} diff --git a/Mastodon/Scene/Profile/Follower/FollowerListViewModel+State.swift b/Mastodon/Scene/Profile/Follower/FollowerListViewModel+State.swift index eeb285917..e25ff867f 100644 --- a/Mastodon/Scene/Profile/Follower/FollowerListViewModel+State.swift +++ b/Mastodon/Scene/Profile/Follower/FollowerListViewModel+State.swift @@ -144,6 +144,14 @@ extension FollowerListViewModel.State { authenticationBox: viewModel.authContext.mastodonAuthenticationBox ) + if accountResponse.value.isEmpty { + await enter(state: NoMore.self) + + viewModel.accounts = [] + viewModel.relationships = [] + return + } + var hasNewAppend = false let newRelationships = try await viewModel.context.apiService.relationship(forAccounts: accountResponse.value, authenticationBox: viewModel.authContext.mastodonAuthenticationBox) diff --git a/Mastodon/Scene/Profile/Follower/FollowerListViewModel.swift b/Mastodon/Scene/Profile/Follower/FollowerListViewModel.swift index df0751a3c..b0d31417a 100644 --- a/Mastodon/Scene/Profile/Follower/FollowerListViewModel.swift +++ b/Mastodon/Scene/Profile/Follower/FollowerListViewModel.swift @@ -22,11 +22,11 @@ final class FollowerListViewModel { @Published var accounts: [Mastodon.Entity.Account] @Published var relationships: [Mastodon.Entity.Relationship] - let listBatchFetchViewModel: ListBatchFetchViewModel - @Published var domain: String? @Published var userID: String? - + + let shouldFetch = PassthroughSubject() + var tableView: UITableView? // output @@ -56,6 +56,5 @@ final class FollowerListViewModel { self.userID = userID self.accounts = [] self.relationships = [] - self.listBatchFetchViewModel = ListBatchFetchViewModel() } } diff --git a/Mastodon/Scene/Profile/Following/FollowingListViewController.swift b/Mastodon/Scene/Profile/Following/FollowingListViewController.swift index ecd26fb34..28fdac266 100644 --- a/Mastodon/Scene/Profile/Following/FollowingListViewController.swift +++ b/Mastodon/Scene/Profile/Following/FollowingListViewController.swift @@ -173,7 +173,6 @@ extension FollowingListViewController: UIScrollViewDelegate { if visibleBottomY > fetchThrottleOffsetY { viewModel.shouldFetch.send() } - } }