From 9975fd56d9090ae600e178b97ca84e7fab5aba91 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Thu, 9 Nov 2023 21:55:36 +0100 Subject: [PATCH] Make "Followed Hashtags"-screen work with entities (IOS-186) --- Mastodon/Coordinator/SceneCoordinator.swift | 6 +- .../FollowedTagsTableViewCell.swift | 17 ++-- .../FollowedTagsViewController.swift | 82 +++++++++++-------- ...owedTagsViewModel+DiffableDataSource.swift | 10 +-- .../FollowedTags/FollowedTagsViewModel.swift | 58 ++++--------- .../Service/API/APIService+Account.swift | 23 +----- 6 files changed, 83 insertions(+), 113 deletions(-) diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index a7b32feb4..d74a4ccd8 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -448,9 +448,9 @@ private extension SceneCoordinator { _viewController.viewModel = viewModel viewController = _viewController case .followedTags(let viewModel): - let _viewController = FollowedTagsViewController() - _viewController.viewModel = viewModel - viewController = _viewController + guard let authContext else { return nil } + + viewController = FollowedTagsViewController(appContext: appContext, sceneCoordinator: self, authContext: authContext, viewModel: viewModel) case .favorite(let viewModel): let _viewController = FavoriteViewController() _viewController.viewModel = viewModel diff --git a/Mastodon/Scene/Profile/FollowedTags/FollowedTagsTableViewCell.swift b/Mastodon/Scene/Profile/FollowedTags/FollowedTagsTableViewCell.swift index 6adb15a9c..3ae6b3ed8 100644 --- a/Mastodon/Scene/Profile/FollowedTags/FollowedTagsTableViewCell.swift +++ b/Mastodon/Scene/Profile/FollowedTags/FollowedTagsTableViewCell.swift @@ -6,23 +6,24 @@ // import UIKit -import CoreDataStack +import MastodonSDK final class FollowedTagsTableViewCell: UITableViewCell { + + static let reuseIdentifier = "FollowedTagsTableViewCell" + private var hashtagView: HashtagTimelineHeaderView! private let separatorLine = UIView.separatorLine - private weak var viewModel: FollowedTagsViewModel? - private weak var hashtag: Tag? + private var viewModel: FollowedTagsViewModel? + private var hashtag: Mastodon.Entity.Tag? override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) setup() } - required init?(coder: NSCoder) { - fatalError("Not implemented") - } - + required init?(coder: NSCoder) { fatalError("Not implemented") } + override func prepareForReuse() { hashtagView.removeFromSuperview() viewModel = nil @@ -67,7 +68,7 @@ private extension FollowedTagsTableViewCell { } extension FollowedTagsTableViewCell { - func populate(with tag: Tag) { + func populate(with tag: Mastodon.Entity.Tag) { self.hashtag = tag hashtagView.update(HashtagTimelineHeaderView.Data.from(tag)) } diff --git a/Mastodon/Scene/Profile/FollowedTags/FollowedTagsViewController.swift b/Mastodon/Scene/Profile/FollowedTags/FollowedTagsViewController.swift index 4edec01d9..7e2bf5095 100644 --- a/Mastodon/Scene/Profile/FollowedTags/FollowedTagsViewController.swift +++ b/Mastodon/Scene/Profile/FollowedTags/FollowedTagsViewController.swift @@ -14,52 +14,70 @@ import MastodonUI import MastodonLocalization final class FollowedTagsViewController: UIViewController, NeedsDependency { - weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } - weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } - + var context: AppContext! + var coordinator: SceneCoordinator! + let authContext: AuthContext + var disposeBag = Set() - var viewModel: FollowedTagsViewModel! + var viewModel: FollowedTagsViewModel let titleView = DoubleTitleLabelNavigationBarTitleView() + let tableView: UITableView - lazy var tableView: UITableView = { - let tableView = UITableView() - tableView.register(FollowedTagsTableViewCell.self, forCellReuseIdentifier: String(describing: FollowedTagsTableViewCell.self)) + init(appContext: AppContext, sceneCoordinator: SceneCoordinator, authContext: AuthContext, viewModel: FollowedTagsViewModel) { + self.context = appContext + self.coordinator = sceneCoordinator + self.authContext = authContext + self.viewModel = viewModel + + tableView = UITableView() + tableView.register(FollowedTagsTableViewCell.self, forCellReuseIdentifier: FollowedTagsTableViewCell.reuseIdentifier) + tableView.translatesAutoresizingMaskIntoConstraints = false tableView.rowHeight = UITableView.automaticDimension tableView.separatorStyle = .none tableView.backgroundColor = .clear - return tableView - }() - -} -extension FollowedTagsViewController { - override func viewDidLoad() { - super.viewDidLoad() - - let _title = L10n.Scene.FollowedTags.title - title = _title - titleView.update(title: _title, subtitle: nil) + super.init(nibName: nil, bundle: nil) + + let title = L10n.Scene.FollowedTags.title + self.title = title + titleView.update(title: title, subtitle: nil) navigationItem.titleView = titleView - + view.backgroundColor = .secondarySystemBackground - tableView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(tableView) tableView.pinToParent() + + tableView.delegate = self + } + + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + override func viewDidLoad() { + super.viewDidLoad() viewModel.setupTableView(tableView) - - viewModel.presentHashtagTimeline - .receive(on: DispatchQueue.main) - .sink { [weak self] hashtagTimelineViewModel in - guard let self = self else { return } - _ = self.coordinator.present( - scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel), - from: self, - transition: .show - ) - } - .store(in: &disposeBag) + } +} + +extension FollowedTagsViewController: UITableViewDelegate { + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + + let object = viewModel.followedTags[indexPath.row] + + let hashtagTimelineViewModel = HashtagTimelineViewModel( + context: self.context, + authContext: self.authContext, + hashtag: object.name + ) + + _ = self.coordinator.present( + scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel), + from: self, + transition: .show + ) + } } diff --git a/Mastodon/Scene/Profile/FollowedTags/FollowedTagsViewModel+DiffableDataSource.swift b/Mastodon/Scene/Profile/FollowedTags/FollowedTagsViewModel+DiffableDataSource.swift index 91f2cc5e9..9518768dc 100644 --- a/Mastodon/Scene/Profile/FollowedTags/FollowedTagsViewModel+DiffableDataSource.swift +++ b/Mastodon/Scene/Profile/FollowedTags/FollowedTagsViewModel+DiffableDataSource.swift @@ -7,8 +7,6 @@ import UIKit import Combine -import CoreData -import CoreDataStack import MastodonSDK import MastodonCore @@ -18,7 +16,7 @@ extension FollowedTagsViewModel { } enum Item: Hashable { - case hashtag(Tag) + case hashtag(Mastodon.Entity.Tag) } func tableViewDiffableDataSource( @@ -27,7 +25,7 @@ extension FollowedTagsViewModel { 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 { + guard let cell = tableView.dequeueReusableCell(withIdentifier: FollowedTagsTableViewCell.reuseIdentifier, for: indexPath) as? FollowedTagsTableViewCell else { assertionFailure() return UITableViewCell() } @@ -39,9 +37,7 @@ extension FollowedTagsViewModel { } } - func setupDiffableDataSource( - tableView: UITableView - ) { + func setupDiffableDataSource(tableView: UITableView) { diffableDataSource = tableViewDiffableDataSource(for: tableView) var snapshot = NSDiffableDataSourceSnapshot() diff --git a/Mastodon/Scene/Profile/FollowedTags/FollowedTagsViewModel.swift b/Mastodon/Scene/Profile/FollowedTags/FollowedTagsViewModel.swift index f73094a8c..677408ac1 100644 --- a/Mastodon/Scene/Profile/FollowedTags/FollowedTagsViewModel.swift +++ b/Mastodon/Scene/Profile/FollowedTags/FollowedTagsViewModel.swift @@ -8,14 +8,12 @@ import os import UIKit import Combine -import CoreData -import CoreDataStack import MastodonSDK import MastodonCore final class FollowedTagsViewModel: NSObject { var disposeBag = Set() - let fetchedResultsController: FollowedTagsFetchedResultController + private(set) var followedTags: [Mastodon.Entity.Tag] private weak var tableView: UITableView? var diffableDataSource: UITableViewDiffableDataSource? @@ -30,78 +28,52 @@ final class FollowedTagsViewModel: NSObject { init(context: AppContext, authContext: AuthContext) { self.context = context self.authContext = authContext - self.fetchedResultsController = FollowedTagsFetchedResultController( - managedObjectContext: context.managedObjectContext, - domain: authContext.mastodonAuthenticationBox.domain, - user: authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext)! // fixme: - ) + self.followedTags = [] super.init() - - self.fetchedResultsController - .$records - .receive(on: DispatchQueue.main) - .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?.apply(snapshot, animatingDifferences: true) - } - .store(in: &disposeBag) } } extension FollowedTagsViewModel { func setupTableView(_ tableView: UITableView) { - self.tableView = tableView setupDiffableDataSource(tableView: tableView) - tableView.delegate = self fetchFollowedTags() } func fetchFollowedTags() { Task { @MainActor in - try await context.apiService.getFollowedTags( + followedTags = try await context.apiService.getFollowedTags( domain: authContext.mastodonAuthenticationBox.domain, query: Mastodon.API.Account.FollowedTagsQuery(limit: nil), authenticationBox: authContext.mastodonAuthenticationBox - ) + ).value + + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.main]) + let items = followedTags.compactMap { Item.hashtag($0) } + snapshot.appendItems(items, toSection: .main) + + await diffableDataSource?.apply(snapshot) } } - func followOrUnfollow(_ tag: Tag) { + func followOrUnfollow(_ tag: Mastodon.Entity.Tag) { Task { @MainActor in - switch tag.following { - case true: + if tag.following ?? false { _ = try? await context.apiService.unfollowTag( for: tag.name, authenticationBox: authContext.mastodonAuthenticationBox ) - case false: + } else { _ = try? await context.apiService.followTag( for: tag.name, authenticationBox: authContext.mastodonAuthenticationBox ) } + fetchFollowedTags() } } } -extension FollowedTagsViewModel: UITableViewDelegate { - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - tableView.deselectRow(at: indexPath, animated: true) - - 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/Service/API/APIService+Account.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift index ad9561565..5e058c258 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift @@ -158,32 +158,15 @@ extension APIService { let domain = authenticationBox.domain let authorization = authenticationBox.userAuthorization - let response = try await Mastodon.API.Account.followedTags( + let followedTags = try await Mastodon.API.Account.followedTags( session: session, domain: domain, query: query, authorization: authorization ).singleOutput() - - let managedObjectContext = self.backgroundManagedObjectContext - try await managedObjectContext.performChanges { - let me = authenticationBox.authentication.user(in: managedObjectContext) - for entity in response.value { - _ = Persistence.Tag.createOrMerge( - in: managedObjectContext, - context: Persistence.Tag.PersistContext( - domain: domain, - entity: entity, - me: me, - networkDate: response.networkDate - ) - ) - } - } - - return response - } // end func + return followedTags + } } extension APIService {