[PM-28144] Add debouncing to searches (#2176)

This commit is contained in:
Federico Maccaroni 2025-12-03 10:49:14 -03:00 committed by GitHub
parent 22bf62f9ac
commit 446858c7fb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 68 additions and 6 deletions

View File

@ -371,7 +371,7 @@ struct ItemListView: View {
placement: .navigationBarDrawer(displayMode: .always),
prompt: Localizations.search,
)
.task(id: store.state.searchText) {
.searchDebouncedTask(id: store.state.searchText) {
await store.perform(.search(store.state.searchText))
}
}

View File

@ -24,6 +24,9 @@ public enum Constants {
/// The minimum number of minutes before attempting a server config sync again.
public static let minimumConfigSyncInterval: TimeInterval = 60 * 60 // 60 minutes
/// The search debounce time in nanoseconds.
public static let searchDebounceTimeInNS: UInt64 = 200_000_000 // 200ms
/// The default file name when the file name cannot be determined.
public static let unknownFileName = "unknown_file_name"
}

View File

@ -0,0 +1,59 @@
import SwiftUI
public extension View {
/// Adds a debounced task to perform before this view appears or when a specified
/// value changes.
/// This means that the task will execute the action when the specified value doesn't change
/// in the debounce interval specified.
///
/// - Parameters:
/// - id: The value to observe for changes. The value must conform
/// to the <doc://com.apple.documentation/documentation/Swift/Equatable>
/// protocol.
/// - debounceIntervalInNS: The interval to be set for debouncing the task.
/// - action: A closure that SwiftUI calls as an asynchronous task
/// before the view appears. SwiftUI can automatically cancel the task
/// after the view disappears before the action completes. If the
/// `id` value changes, SwiftUI cancels and restarts the task.
///
/// - Returns: A view that runs the specified action asynchronously before
/// the view appears, or restarts the task when the `id` value changes.
@inlinable nonisolated func debouncedTask<T>(
id value: T,
debounceIntervalInNS: UInt64,
_ action: @escaping @Sendable () async -> Void,
) -> some View where T: Equatable {
task(id: value) {
try? await Task.sleep(nanoseconds: debounceIntervalInNS)
guard !Task.isCancelled else {
return
}
await action()
}
}
/// Adds a task to perform before this view appears or when a specified
/// value changes to be used on debounced searches.
///
/// - Parameters:
/// - id: The value to observe for changes. The value must conform
/// to the <doc://com.apple.documentation/documentation/Swift/Equatable>
/// protocol.
/// - action: A closure that SwiftUI calls as an asynchronous task
/// before the view appears. SwiftUI can automatically cancel the task
/// after the view disappears before the action completes. If the
/// `id` value changes, SwiftUI cancels and restarts the task.
///
/// - Returns: A view that runs the specified action asynchronously before
/// the view appears, or restarts the task when the `id` value changes.
@inlinable nonisolated func searchDebouncedTask<T>(
id value: T,
_ action: @escaping @Sendable () async -> Void,
) -> some View where T: Equatable {
debouncedTask(
id: value,
debounceIntervalInNS: Constants.searchDebounceTimeInNS,
action
)
}
}

View File

@ -266,7 +266,7 @@ struct SendListView: View {
)
.task { await store.perform(.loadData) }
.task { await store.perform(.streamSendList) }
.task(id: store.state.searchText) {
.searchDebouncedTask(id: store.state.searchText) {
await store.perform(.search(store.state.searchText))
}
.onChange(of: store.state.infoUrl) { newValue in

View File

@ -106,7 +106,7 @@ private struct VaultAutofillListSearchableView: View {
.task {
await store.perform(.streamShowWebIcons)
}
.task(id: store.state.searchText) {
.searchDebouncedTask(id: store.state.searchText) {
await store.perform(.search(store.state.searchText))
}
.task(id: store.state.excludedCredentialIdFound) {

View File

@ -62,7 +62,7 @@ struct VaultGroupView: View {
openURL(url)
store.send(.clearURL)
}
.task(id: store.state.searchText) {
.searchDebouncedTask(id: store.state.searchText) {
await store.perform(.search(store.state.searchText))
}
.task(id: store.state.searchVaultFilterType) {

View File

@ -133,7 +133,7 @@ private struct VaultItemSelectionSearchableView: View {
.task {
await store.perform(.streamVaultItems)
}
.task(id: store.state.searchText) {
.searchDebouncedTask(id: store.state.searchText) {
await store.perform(.search(store.state.searchText))
}
.toast(

View File

@ -334,7 +334,7 @@ struct VaultListView: View {
prompt: Localizations.search,
)
.autocorrectionDisabled(true)
.task(id: store.state.searchText) {
.searchDebouncedTask(id: store.state.searchText) {
await store.perform(.search(store.state.searchText))
}
.task(id: store.state.searchVaultFilterType) {