diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index e94871b4d..c5375b7a0 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -23,6 +23,7 @@ 0FB3D33825E6401400AAD544 /* PickServerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D33725E6401400AAD544 /* PickServerCell.swift */; }; 164F0EBC267D4FE400249499 /* BoopSound.caf in Resources */ = {isa = PBXBuildFile; fileRef = 164F0EBB267D4FE400249499 /* BoopSound.caf */; }; 18BC7629F65E6DB12CB8416D /* Pods_Mastodon_MastodonUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */; }; + 2A1FE47C2938BB2600784BF1 /* FollowedTagsViewModel+DiffableDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A1FE47B2938BB2600784BF1 /* FollowedTagsViewModel+DiffableDataSource.swift */; }; 2A3F6FE3292ECB5E002E6DA7 /* FollowedTagsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3F6FE2292ECB5E002E6DA7 /* FollowedTagsViewModel.swift */; }; 2A3F6FE5292F6E44002E6DA7 /* FollowedTagsTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3F6FE4292F6E44002E6DA7 /* FollowedTagsTableViewCell.swift */; }; 2A506CF4292CD85800059C37 /* FollowedTagsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A506CF3292CD85800059C37 /* FollowedTagsViewController.swift */; }; @@ -525,6 +526,7 @@ 0FB3D33725E6401400AAD544 /* PickServerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerCell.swift; sourceTree = ""; }; 164F0EBB267D4FE400249499 /* BoopSound.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = BoopSound.caf; sourceTree = ""; }; 1D6D967E77A5357E2C6110D9 /* Pods-Mastodon.asdk - debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.asdk - debug.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.asdk - debug.xcconfig"; sourceTree = ""; }; + 2A1FE47B2938BB2600784BF1 /* FollowedTagsViewModel+DiffableDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FollowedTagsViewModel+DiffableDataSource.swift"; sourceTree = ""; }; 2A3F6FE2292ECB5E002E6DA7 /* FollowedTagsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowedTagsViewModel.swift; sourceTree = ""; }; 2A3F6FE4292F6E44002E6DA7 /* FollowedTagsTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowedTagsTableViewCell.swift; sourceTree = ""; }; 2A506CF3292CD85800059C37 /* FollowedTagsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowedTagsViewController.swift; sourceTree = ""; }; @@ -1243,6 +1245,7 @@ children = ( 2A506CF3292CD85800059C37 /* FollowedTagsViewController.swift */, 2A3F6FE2292ECB5E002E6DA7 /* FollowedTagsViewModel.swift */, + 2A1FE47B2938BB2600784BF1 /* FollowedTagsViewModel+DiffableDataSource.swift */, 2A3F6FE4292F6E44002E6DA7 /* FollowedTagsTableViewCell.swift */, ); path = FollowedTags; @@ -3398,6 +3401,7 @@ DB938EED2623F79B00E5B6C1 /* ThreadViewModel.swift in Sources */, DB6988DE2848D11C002398EF /* PagerTabStripNavigateable.swift in Sources */, 2DCB73FD2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift in Sources */, + 2A1FE47C2938BB2600784BF1 /* FollowedTagsViewModel+DiffableDataSource.swift in Sources */, DB852D1C26FB021500FC9D81 /* RootSplitViewController.swift in Sources */, DB697DD1278F4871004EF2F7 /* AutoGenerateTableViewDelegate.swift in Sources */, DB02CDBF2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift in Sources */, diff --git a/Mastodon/Scene/Profile/FollowedTags/FollowedTagsViewModel+DiffableDataSource.swift b/Mastodon/Scene/Profile/FollowedTags/FollowedTagsViewModel+DiffableDataSource.swift new file mode 100644 index 000000000..91f2cc5e9 --- /dev/null +++ b/Mastodon/Scene/Profile/FollowedTags/FollowedTagsViewModel+DiffableDataSource.swift @@ -0,0 +1,51 @@ +// +// FollowedTagsViewModel+DiffableDataSource.swift +// Mastodon +// +// Created by Marcus Kida on 01.12.22. +// + +import UIKit +import Combine +import CoreData +import CoreDataStack +import MastodonSDK +import MastodonCore + +extension FollowedTagsViewModel { + enum Section: Hashable { + case main + } + + enum Item: Hashable { + case hashtag(Tag) + } + + func tableViewDiffableDataSource( + for tableView: UITableView + ) -> UITableViewDiffableDataSource { + UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item in + switch item { + case let .hashtag(tag): + guard let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: FollowedTagsTableViewCell.self), for: indexPath) as? FollowedTagsTableViewCell else { + assertionFailure() + return UITableViewCell() + } + + cell.setup(self) + cell.populate(with: tag) + return cell + } + } + } + + func setupDiffableDataSource( + tableView: UITableView + ) { + diffableDataSource = tableViewDiffableDataSource(for: tableView) + + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.main]) + diffableDataSource?.apply(snapshot) + } +} diff --git a/Mastodon/Scene/Profile/FollowedTags/FollowedTagsViewModel.swift b/Mastodon/Scene/Profile/FollowedTags/FollowedTagsViewModel.swift index e0f1b326a..75464092e 100644 --- a/Mastodon/Scene/Profile/FollowedTags/FollowedTagsViewModel.swift +++ b/Mastodon/Scene/Profile/FollowedTags/FollowedTagsViewModel.swift @@ -19,7 +19,8 @@ final class FollowedTagsViewModel: NSObject { let fetchedResultsController: FollowedTagsFetchedResultController private weak var tableView: UITableView? - + var diffableDataSource: UITableViewDiffableDataSource? + // input let context: AppContext let authContext: AuthContext @@ -35,12 +36,18 @@ final class FollowedTagsViewModel: NSObject { domain: authContext.mastodonAuthenticationBox.domain, user: authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)!.user ) + super.init() + self.fetchedResultsController .$records .receive(on: DispatchQueue.main) - .sink { [weak self] _ in - self?.tableView?.reloadSections(IndexSet(integer: 0), with: .automatic) + .sink { [weak self] records in + guard let self = self else { return } + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.main]) + snapshot.appendItems(records.map {.hashtag($0) }) + self.diffableDataSource?.applySnapshot(snapshot, animated: true) } .store(in: &disposeBag) } @@ -49,29 +56,22 @@ final class FollowedTagsViewModel: NSObject { extension FollowedTagsViewModel { func setupTableView(_ tableView: UITableView) { self.tableView = tableView - tableView.dataSource = self + setupDiffableDataSource(tableView: tableView) tableView.delegate = self - fetchFollowedTags { - tableView.reloadData() + fetchFollowedTags() + } + + func fetchFollowedTags() { + Task { @MainActor in + try await context.apiService.getFollowedTags( + domain: authContext.mastodonAuthenticationBox.domain, + query: Mastodon.API.Account.FollowedTagsQuery(limit: nil), + authenticationBox: authContext.mastodonAuthenticationBox + ) } } - - func fetchFollowedTags(_ done: @escaping () -> Void) { - Task { @MainActor [weak self] in - try? await self?._fetchFollowedTags() - done() - } - } - - private func _fetchFollowedTags() async throws { - try await context.apiService.getFollowedTags( - domain: authContext.mastodonAuthenticationBox.domain, - query: Mastodon.API.Account.FollowedTagsQuery(limit: nil), - authenticationBox: authContext.mastodonAuthenticationBox - ) - } - + func followOrUnfollow(_ tag: Tag) { Task { @MainActor in switch tag.following { @@ -86,53 +86,24 @@ extension FollowedTagsViewModel { authenticationBox: authContext.mastodonAuthenticationBox ) } - try? await _fetchFollowedTags() + fetchFollowedTags() } } } -extension FollowedTagsViewModel: UITableViewDataSource { - func numberOfSections(in tableView: UITableView) -> Int { - 1 - } - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - fetchedResultsController.records.count - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard - indexPath.section == 0, - let object = fetchedResultsController.records[indexPath.row].object(in: context.managedObjectContext) - else { - return UITableViewCell() - } - - let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: FollowedTagsTableViewCell.self), for: indexPath) as! FollowedTagsTableViewCell - - cell.setup(self) - cell.populate(with: object) - return cell - } -} - extension FollowedTagsViewModel: UITableViewDelegate { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(indexPath)") tableView.deselectRow(at: indexPath, animated: true) - guard - indexPath.section == 0, - let object = fetchedResultsController.records[indexPath.row].object(in: context.managedObjectContext) - else { - return - } + let object = fetchedResultsController.records[indexPath.row] let hashtagTimelineViewModel = HashtagTimelineViewModel( context: self.context, authContext: self.authContext, hashtag: object.name ) + presentHashtagTimeline.send(hashtagTimelineViewModel) } } diff --git a/MastodonSDK/Sources/MastodonCore/FetchedResultsController/FollowedTagsFetchedResultController.swift b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/FollowedTagsFetchedResultController.swift index 1398cc7f6..8c5c64b58 100644 --- a/MastodonSDK/Sources/MastodonCore/FetchedResultsController/FollowedTagsFetchedResultController.swift +++ b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/FollowedTagsFetchedResultController.swift @@ -23,8 +23,7 @@ public final class FollowedTagsFetchedResultController: NSObject { @Published public var user: MastodonUser? = nil // output - let _objectIDs = CurrentValueSubject<[NSManagedObjectID], Never>([]) - @Published public private(set) var records: [ManagedObjectRecord] = [] + @Published public private(set) var records: [Tag] = [] public init(managedObjectContext: NSManagedObjectContext, domain: String, user: MastodonUser) { self.domain = domain @@ -44,13 +43,7 @@ public final class FollowedTagsFetchedResultController: NSObject { return controller }() super.init() - - // debounce output to prevent UI update issues - _objectIDs - .throttle(for: 0.1, scheduler: DispatchQueue.main, latest: true) - .map { objectIDs in objectIDs.map { ManagedObjectRecord(objectID: $0) } } - .assign(to: &$records) - + fetchedResultsController.delegate = self try? fetchedResultsController.performFetch() @@ -75,11 +68,10 @@ public final class FollowedTagsFetchedResultController: NSObject { // MARK: - NSFetchedResultsControllerDelegate extension FollowedTagsFetchedResultController: NSFetchedResultsControllerDelegate { - public func controllerDidChangeContent(_ controller: NSFetchedResultsController) { + public func controller(_ controller: NSFetchedResultsController, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) { os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) let objects = fetchedResultsController.fetchedObjects ?? [] - self._objectIDs.value = objects.map { $0.objectID } - + self.records = objects } }