From a0f7454a3d4ccde89d10cbbe27eeed9e0a1a1eea Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Wed, 20 Sep 2023 18:47:35 +0200 Subject: [PATCH] Show loading-indicator (IOS-141) aaaaand simplify things as we don't need a super-dynamic search-result-screen anymore. --- .../SearchResultOverviewCoordinator.swift | 6 +- .../SearchResult/SearchResultSection.swift | 2 - ...ultViewController+DataSourceProvider.swift | 2 + .../SearchResultViewController.swift | 85 +------------------ .../SearchResultViewModel+Diffable.swift | 6 +- .../SearchResultViewModel+State.swift | 35 ++++---- .../SearchResult/SearchResultViewModel.swift | 9 +- 7 files changed, 31 insertions(+), 114 deletions(-) diff --git a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultOverviewCoordinator.swift b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultOverviewCoordinator.swift index 4dc337b9a..a2d6c8e24 100644 --- a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultOverviewCoordinator.swift +++ b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultOverviewCoordinator.swift @@ -34,8 +34,7 @@ class SearchResultOverviewCoordinator: Coordinator { extension SearchResultOverviewCoordinator: SearchResultsOverviewTableViewControllerDelegate { @MainActor func searchForPosts(_ viewController: SearchResultsOverviewTableViewController, withSearchText searchText: String) { - let searchResultViewModel = SearchResultViewModel(context: context, authContext: authContext, searchScope: .posts) - searchResultViewModel.searchText.value = searchText + let searchResultViewModel = SearchResultViewModel(context: context, authContext: authContext, searchScope: .posts, searchText: searchText) sceneCoordinator.present(scene: .searchResult(viewModel: searchResultViewModel), transition: .show) } @@ -54,8 +53,7 @@ extension SearchResultOverviewCoordinator: SearchResultsOverviewTableViewControl @MainActor func searchForPeople(_ viewController: SearchResultsOverviewTableViewController, withName searchText: String) { - let searchResultViewModel = SearchResultViewModel(context: context, authContext: authContext, searchScope: .people) - searchResultViewModel.searchText.value = searchText + let searchResultViewModel = SearchResultViewModel(context: context, authContext: authContext, searchScope: .people, searchText: searchText) sceneCoordinator.present(scene: .searchResult(viewModel: searchResultViewModel), transition: .show) } diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultSection.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultSection.swift index a7b10ae1f..dbede1795 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultSection.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultSection.swift @@ -23,8 +23,6 @@ enum SearchResultSection: Hashable { extension SearchResultSection { - static let logger = Logger(subsystem: "SearchResultSection", category: "logic") - struct Configuration { let authContext: AuthContext weak var statusViewTableViewCellDelegate: StatusTableViewCellDelegate? diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+DataSourceProvider.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+DataSourceProvider.swift index a2f41aefb..f2e8c7c6e 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+DataSourceProvider.swift @@ -71,6 +71,8 @@ extension SearchResultViewController { case .notification: assertionFailure() } // end switch + + tableView.deselectRow(at: indexPath, animated: true) } // end Task } // end func } diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController.swift index 731117ec6..fa8b844a6 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController.swift @@ -11,6 +11,7 @@ import Combine import CoreDataStack import MastodonCore import MastodonUI +import MastodonAsset final class SearchResultViewController: UIViewController, NeedsDependency, MediaPreviewableViewController { @@ -37,14 +38,7 @@ extension SearchResultViewController { override func viewDidLoad() { super.viewDidLoad() - setupBackgroundColor(theme: ThemeService.shared.currentTheme.value) - ThemeService.shared.currentTheme - .receive(on: DispatchQueue.main) - .sink { [weak self] theme in - guard let self = self else { return } - self.setupBackgroundColor(theme: theme) - } - .store(in: &disposeBag) + view.backgroundColor = Asset.Theme.System.systemGroupedBackground.color tableView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(tableView) @@ -68,85 +62,14 @@ extension SearchResultViewController { } .store(in: &disposeBag) - // listen keyboard events and set content inset - let keyboardEventPublishers = Publishers.CombineLatest3( - KeyboardResponderService.shared.isShow, - KeyboardResponderService.shared.state, - KeyboardResponderService.shared.endFrame - ) - Publishers.CombineLatest3( - keyboardEventPublishers, - viewModel.viewDidAppear, - viewModel.didDataSourceUpdate - ) - .sink(receiveValue: { [weak self] keyboardEvents, _, _ in - guard let self = self else { return } - let (isShow, state, endFrame) = keyboardEvents - - // update keyboard background color - guard isShow, state == .dock else { - self.tableView.contentInset.bottom = 0 - self.tableView.verticalScrollIndicatorInsets.bottom = 0 - return - } - // isShow AND dock state - - // adjust inset for tableView - let contentFrame = self.view.convert(self.tableView.frame, to: nil) - let padding = contentFrame.maxY - endFrame.minY - guard padding > 0 else { - self.tableView.contentInset.bottom = self.view.safeAreaInsets.bottom - self.tableView.verticalScrollIndicatorInsets.bottom = self.view.safeAreaInsets.bottom - return - } - - self.tableView.contentInset.bottom = padding - self.view.safeAreaInsets.bottom - self.tableView.verticalScrollIndicatorInsets.bottom = padding - self.view.safeAreaInsets.bottom - }) - .store(in: &disposeBag) -// - // works for already onscreen page - viewModel.navigationBarFrame - .removeDuplicates() - .receive(on: DispatchQueue.main) - .sink { [weak self] frame in - guard let self = self else { return } - guard self.viewModel.viewDidAppear.value else { return } - self.tableView.contentInset.top = frame.height - self.tableView.verticalScrollIndicatorInsets.top = frame.height - } - .store(in: &disposeBag) - - title = viewModel.searchText.value - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - // works for appearing page - if !viewModel.viewDidAppear.value { - tableView.contentInset.top = viewModel.navigationBarFrame.value.height - tableView.contentOffset.y = -viewModel.navigationBarFrame.value.height - } - - tableView.deselectRow(with: transitionCoordinator, animated: animated) + title = viewModel.searchText } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - viewModel.viewDidAppear.value = true + viewModel.stateMachine.enter(SearchResultViewModel.State.Initial.self) } - -} - -extension SearchResultViewController { - private func setupBackgroundColor(theme: Theme) { - view.backgroundColor = theme.systemGroupedBackgroundColor -// tableView.backgroundColor = theme.systemBackgroundColor -// searchHeader.backgroundColor = theme.systemGroupedBackgroundColor - } - } // MARK: - StatusTableViewCellDelegate diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+Diffable.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+Diffable.swift index 225f79b02..5b74ba8aa 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+Diffable.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+Diffable.swift @@ -65,8 +65,7 @@ extension SearchResultViewModel { if let currentState = self.stateMachine.currentState { switch currentState { case is State.Loading, - is State.Fail, - is State.Idle: + is State.Fail: let attribute = SearchResultItem.BottomLoaderAttribute(isEmptyResult: false) snapshot.appendItems([.bottomLoader(attribute: attribute)], toSection: .main) case is State.NoMore: @@ -74,6 +73,9 @@ extension SearchResultViewModel { let attribute = SearchResultItem.BottomLoaderAttribute(isEmptyResult: true) snapshot.appendItems([.bottomLoader(attribute: attribute)], toSection: .main) } + case is State.Idle: + // do nothing + break default: break } diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift index 47832e156..4858f0d2d 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift @@ -22,7 +22,7 @@ extension SearchResultViewModel { } @MainActor - func enter(state: State.Type) { + public func enter(state: State.Type) { stateMachine?.enter(state) } } @@ -31,24 +31,27 @@ extension SearchResultViewModel { extension SearchResultViewModel.State { class Initial: SearchResultViewModel.State { override func isValidNextState(_ stateClass: AnyClass) -> Bool { - guard let viewModel = viewModel else { return false } - return stateClass == Loading.self && !viewModel.searchText.value.isEmpty + return stateClass == Loading.self + } + + override func didEnter(from previousState: GKState?) { + super.didEnter(from: previousState) + + guard let viewModel else { return } + + viewModel.items = [.bottomLoader(attribute: .init(isEmptyResult: false))] } } class Loading: SearchResultViewModel.State { - var previousSearchText = "" var offset: Int? = nil var latestLoadingToken = UUID() override func isValidNextState(_ stateClass: AnyClass) -> Bool { - guard let viewModel = self.viewModel else { return false } switch stateClass { case is Fail.Type, is Idle.Type, is NoMore.Type: return true - case is Loading.Type: - return viewModel.searchText.value != previousSearchText default: return false } @@ -56,12 +59,11 @@ extension SearchResultViewModel.State { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) - guard let viewModel = viewModel, let stateMachine = stateMachine else { return } + guard let viewModel, let stateMachine = stateMachine else { return } - let searchText = viewModel.searchText.value let searchType = viewModel.searchScope.searchType - if previousState is NoMore && previousSearchText == searchText { + if previousState is NoMore { // same searchText from NoMore // break the loading and resume NoMore state stateMachine.enter(NoMore.self) @@ -71,17 +73,12 @@ extension SearchResultViewModel.State { // viewModel.items.value = viewModel.items.value } - guard !searchText.isEmpty else { + guard viewModel.searchText.isEmpty == false else { stateMachine.enter(Fail.self) return } - if searchText != previousSearchText { - previousSearchText = searchText - offset = nil - } else { - offset = viewModel.items.count - } + offset = viewModel.items.count // not set offset for all case // and assert other cases the items are all the same type elements @@ -93,7 +90,7 @@ extension SearchResultViewModel.State { }() let query = Mastodon.API.V2.Search.Query( - q: searchText, + q: viewModel.searchText, type: searchType, accountID: nil, maxID: nil, @@ -115,8 +112,6 @@ extension SearchResultViewModel.State { authenticationBox: viewModel.authContext.mastodonAuthenticationBox ) - // discard result when search text is outdated - guard searchText == self.previousSearchText else { return } // discard result when request not the latest one guard id == self.latestLoadingToken else { return } // discard result when state is not Loading diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel.swift index 0c5c5868f..f66921535 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel.swift @@ -21,13 +21,12 @@ final class SearchResultViewModel { let context: AppContext let authContext: AuthContext let searchScope: SearchScope - let searchText = CurrentValueSubject("") + let searchText: String @Published var hashtags: [Mastodon.Entity.Tag] = [] let userFetchedResultsController: UserFetchedResultsController let statusFetchedResultsController: StatusFetchedResultsController let listBatchFetchViewModel = ListBatchFetchViewModel() - let viewDidAppear = CurrentValueSubject(false) var cellFrameCache = NSCache() var navigationBarFrame = CurrentValueSubject(.zero) @@ -43,15 +42,16 @@ final class SearchResultViewModel { State.Idle(viewModel: self), State.NoMore(viewModel: self), ]) - stateMachine.enter(State.Initial.self) return stateMachine }() let didDataSourceUpdate = PassthroughSubject() - init(context: AppContext, authContext: AuthContext, searchScope: SearchScope = .all) { + init(context: AppContext, authContext: AuthContext, searchScope: SearchScope = .all, searchText: String) { self.context = context self.authContext = authContext self.searchScope = searchScope + self.searchText = searchText + self.userFetchedResultsController = UserFetchedResultsController( managedObjectContext: context.managedObjectContext, domain: authContext.mastodonAuthenticationBox.domain, @@ -63,5 +63,4 @@ final class SearchResultViewModel { additionalTweetPredicate: nil ) } - }