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:
parent
e041a7e086
commit
a0f7454a3d
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -71,6 +71,8 @@ extension SearchResultViewController {
|
|||
case .notification:
|
||||
assertionFailure()
|
||||
} // end switch
|
||||
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
} // end Task
|
||||
} // end func
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue