diff --git a/Model/Search/SearchModel.swift b/Model/Search/SearchModel.swift index a426f017..ab92c614 100644 --- a/Model/Search/SearchModel.swift +++ b/Model/Search/SearchModel.swift @@ -6,14 +6,14 @@ final class SearchModel: ObservableObject { @Published var store = Store<[ContentItem]>() @Published var page: SearchPage? - var accounts = AccountsModel() @Published var query = SearchQuery() @Published var queryText = "" - @Published var querySuggestions = Store<[String]>() @Published var suggestionsText = "" + @Published var suggestionSelection = "" - @Published var fieldIsFocused = false + @Published var querySuggestions = Store<[String]>() + var accounts = AccountsModel() private var resource: Resource! var isLoading: Bool { @@ -49,10 +49,9 @@ final class SearchModel: ObservableObject { page = nil store.replace([]) - resource = newResource - resource.addObserver(store) - if !query.isEmpty { + resource = newResource + resource.addObserver(store) loadResource() } } @@ -74,7 +73,12 @@ final class SearchModel: ObservableObject { } } - private var suggestionsDebounceTimer: Timer? + var suggestionsResource: Resource? { didSet { + oldValue?.removeObservers(ownedBy: querySuggestions) + oldValue?.cancelLoadIfUnobserved() + + objectWillChange.send() + }} func loadSuggestions(_ query: String) { guard !query.isEmpty else { @@ -82,15 +86,11 @@ final class SearchModel: ObservableObject { return } - suggestionsDebounceTimer?.invalidate() + DispatchQueue.main.async { + self.suggestionsResource = self.accounts.api.searchSuggestions(query: query) + self.suggestionsResource?.addObserver(self.querySuggestions) - suggestionsDebounceTimer = Timer.scheduledTimer(withTimeInterval: 0.2, repeats: false) { _ in - let resource = self.accounts.api.searchSuggestions(query: query) - - resource.addObserver(self.querySuggestions) - resource.loadIfNeeded() - - if let request = resource.loadIfNeeded() { + if let request = self.suggestionsResource?.loadIfNeeded() { request.onSuccess { response in if let suggestions: [String] = response.typedContent() { self.querySuggestions = Store<[String]>(suggestions) diff --git a/Shared/Search/SearchField.swift b/Shared/Search/SearchField.swift index 5f893a99..9eb73df4 100644 --- a/Shared/Search/SearchField.swift +++ b/Shared/Search/SearchField.swift @@ -1,3 +1,4 @@ +import Repeat import SwiftUI struct SearchTextField: View { @@ -7,9 +8,16 @@ struct SearchTextField: View { @EnvironmentObject private var recents @EnvironmentObject private var state + @Binding var queryText: String @Binding var favoriteItem: FavoriteItem? - init(favoriteItem: Binding? = nil) { + private var queryDebouncer = Debouncer(.milliseconds(800)) + + init( + queryText: Binding, + favoriteItem: Binding? = nil + ) { + _queryText = queryText _favoriteItem = favoriteItem ?? .constant(nil) } @@ -28,17 +36,24 @@ struct SearchTextField: View { .padding(.horizontal, 8) .opacity(0.8) #endif - TextField("Search...", text: $state.queryText) { + TextField("Search...", text: $queryText) { state.changeQuery { query in query.query = state.queryText navigation.hideKeyboard() } recents.addQuery(state.queryText, navigation: navigation) } - .onChange(of: state.queryText) { _ in - if state.query.query.compare(state.queryText, options: .caseInsensitive) == .orderedSame { - state.fieldIsFocused = true + .disableAutocorrection(true) + .onChange(of: state.suggestionSelection) { newValue in + self.queryText = newValue + } + .onChange(of: queryText) { newValue in + queryDebouncer.callback = { + DispatchQueue.main.async { + state.queryText = newValue + } } + queryDebouncer.call() } #if os(macOS) .frame(maxWidth: 190) @@ -74,16 +89,14 @@ struct SearchTextField: View { .frame(width: 250, height: 32) .overlay( RoundedRectangle(cornerRadius: 5, style: .continuous) - .stroke( - state.fieldIsFocused ? Color.blue.opacity(0.7) : Color.gray.opacity(0.4), - lineWidth: state.fieldIsFocused ? 3 : 1 - ) + .stroke(Color.gray.opacity(0.4), lineWidth: 1) .frame(width: 250, height: 31) ) } private var clearButton: some View { Button(action: { + queryText = "" self.state.queryText = "" }) { Image(systemName: "xmark.circle.fill") diff --git a/Shared/Search/SearchSuggestions.swift b/Shared/Search/SearchSuggestions.swift index 2ba6806a..1c8ba63a 100644 --- a/Shared/Search/SearchSuggestions.swift +++ b/Shared/Search/SearchSuggestions.swift @@ -8,7 +8,7 @@ struct SearchSuggestions: View { var body: some View { List { Button { - runQueryAction() + runQueryAction(state.queryText) } label: { HStack { Image(systemName: "magnifyingglass") @@ -25,8 +25,7 @@ struct SearchSuggestions: View { ForEach(visibleSuggestions, id: \.self) { suggestion in HStack { Button { - state.queryText = suggestion - runQueryAction() + runQueryAction(suggestion) } label: { HStack { Image(systemName: "magnifyingglass") @@ -52,7 +51,7 @@ struct SearchSuggestions: View { Spacer() Button { - state.queryText = suggestion + state.suggestionSelection = suggestion } label: { Image(systemName: "arrow.up.left.circle") .foregroundColor(.secondary) @@ -72,14 +71,15 @@ struct SearchSuggestions: View { #endif } - private func runQueryAction() { + private func runQueryAction(_ queryText: String) { + state.suggestionSelection = queryText + state.changeQuery { query in - query.query = state.queryText - state.fieldIsFocused = false + query.query = queryText navigation.hideKeyboard() } - recents.addQuery(state.queryText, navigation: navigation) + recents.addQuery(queryText, navigation: navigation) } private var visibleSuggestions: [String] { diff --git a/Shared/Search/SearchView.swift b/Shared/Search/SearchView.swift index 81454741..7070bdc3 100644 --- a/Shared/Search/SearchView.swift +++ b/Shared/Search/SearchView.swift @@ -17,6 +17,7 @@ struct SearchView: View { #endif @State private var favoriteItem: FavoriteItem? + @State private var queryText = "" @Environment(\.navigationStyle) private var navigationStyle @@ -60,9 +61,9 @@ struct SearchView: View { }) { #if os(iOS) VStack { - SearchTextField(favoriteItem: $favoriteItem) + SearchTextField(queryText: $queryText, favoriteItem: $favoriteItem) - if state.query.query != state.queryText, !state.queryText.isEmpty, !state.querySuggestions.collection.isEmpty { + if state.query.query != queryText, !queryText.isEmpty, !state.querySuggestions.collection.isEmpty { SearchSuggestions() } else { results @@ -72,8 +73,8 @@ struct SearchView: View { ZStack { results - #if !os(tvOS) - if state.query.query != state.queryText, !state.queryText.isEmpty, !state.querySuggestions.collection.isEmpty { + #if os(macOS) + if state.query.query != queryText, !queryText.isEmpty, !state.querySuggestions.collection.isEmpty { HStack { Spacer() SearchSuggestions() @@ -107,14 +108,15 @@ struct SearchView: View { filtersMenu } - SearchTextField() + SearchTextField(queryText: $queryText) } #endif } .onAppear { - if query != nil { - state.queryText = query!.query - state.resetQuery(query!) + if let query = query { + queryText = query.query + state.queryText = query.query + state.resetQuery(query) updateFavoriteItem() } @@ -122,19 +124,21 @@ struct SearchView: View { state.store.replace(ContentItem.array(of: videos)) } } - .onChange(of: state.query.query) { newQuery in + .onChange(of: state.queryText) { newQuery in + if queryText.isEmpty, queryText != newQuery { + queryText = newQuery + } + if newQuery.isEmpty { favoriteItem = nil + state.resetQuery() } else { updateFavoriteItem() } - } - .onChange(of: state.queryText) { newQuery in - if newQuery.isEmpty { - state.resetQuery() - } - state.loadSuggestions(newQuery) + if state.query.query != queryText { + state.loadSuggestions(newQuery) + } #if os(tvOS) searchDebounce.invalidate() @@ -152,7 +156,6 @@ struct SearchView: View { } #endif } - .onChange(of: searchSortOrder) { order in state.changeQuery { query in query.sortBy = order @@ -308,7 +311,7 @@ struct SearchView: View { Button { switch item.type { case .query: - state.queryText = item.title + queryText = item.title state.changeQuery { query in query.query = item.title } updateFavoriteItem()