Show loading-indicator (IOS-141)

aaaaand simplify things as we don't need a super-dynamic search-result-screen anymore.
This commit is contained in:
Nathan Mattes 2023-09-20 18:47:35 +02:00
parent e041a7e086
commit a0f7454a3d
7 changed files with 31 additions and 114 deletions

View File

@ -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)
}

View File

@ -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?

View File

@ -71,6 +71,8 @@ extension SearchResultViewController {
case .notification:
assertionFailure()
} // end switch
tableView.deselectRow(at: indexPath, animated: true)
} // end Task
} // end func
}

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -21,13 +21,12 @@ final class SearchResultViewModel {
let context: AppContext
let authContext: AuthContext
let searchScope: SearchScope
let searchText = CurrentValueSubject<String, Never>("")
let searchText: String
@Published var hashtags: [Mastodon.Entity.Tag] = []
let userFetchedResultsController: UserFetchedResultsController
let statusFetchedResultsController: StatusFetchedResultsController
let listBatchFetchViewModel = ListBatchFetchViewModel()
let viewDidAppear = CurrentValueSubject<Bool, Never>(false)
var cellFrameCache = NSCache<NSNumber, NSValue>()
var navigationBarFrame = CurrentValueSubject<CGRect, Never>(.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<Void, Never>()
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
)
}
}