如何在 iOS UISearchBar 中限制搜索(基于打字速度)?
Posted
技术标签:
【中文标题】如何在 iOS UISearchBar 中限制搜索(基于打字速度)?【英文标题】:How to throttle search (based on typing speed) in iOS UISearchBar? 【发布时间】:2014-06-20 14:50:05 【问题描述】:我有一个 UISearchDisplayController 的 UISearchBar 部分,用于显示来自本地 CoreData 和远程 API 的搜索结果。 我想要实现的是对远程 API 的搜索“延迟”。目前,对于用户键入的每个字符,都会发送一个请求。但是如果用户打字特别快,发送很多请求就没有意义:等到他停止打字会有所帮助。 有没有办法做到这一点?
阅读documentation 建议等到用户明确点击搜索,但我认为这并不理想。
性能问题。如果搜索操作可以很 可以快速地更新搜索结果,因为用户是 通过实现 searchBar:textDidChange: 方法在 委托对象。但是,如果搜索操作需要更多时间,您 应该等到用户点击搜索按钮后才开始 在 searchBarSearchButtonClicked: 方法中搜索。始终执行 搜索操作后台线程以避免阻塞主线程 线。这可以让您的应用在搜索时响应用户 运行并提供更好的用户体验。
向 API 发送大量请求不是本地性能问题,而只是避免远程服务器上的请求率过高。
谢谢
【问题讨论】:
我不确定标题是否正确。您要求的是“去抖动”而不是“节流”。 【参考方案1】:试试这个魔法:
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
// to limit network activity, reload half a second after last key press.
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(reload) object:nil];
[self performSelector:@selector(reload) withObject:nil afterDelay:0.5];
Swift 版本:
func searchBar(searchBar: UISearchBar, textDidChange searchText: String)
// to limit network activity, reload half a second after last key press.
NSObject.cancelPreviousPerformRequestsWithTarget(self, selector: "reload", object: nil)
self.performSelector("reload", withObject: nil, afterDelay: 0.5)
注意这个例子调用了一个名为 reload 的方法,但是你可以让它调用任何你喜欢的方法!
【讨论】:
这很好用...不知道 cancelPreviousPerformRequestsWithTarget 方法! 不客气!这是一个很棒的模式,可以用于各种事情。 非常有用!这才是真正的巫术 关于“重新加载”...我不得不再考虑几秒钟...这是指本地方法,它会在用户停止输入后实际执行您想要执行的操作0.5 秒。该方法可以随心所欲地调用,例如 searchExecute。谢谢! 这对我不起作用......每次更改字母时它都会继续运行“重新加载”功能【参考方案2】:对于 Swift 4 以后需要此功能的人:
使用DispatchWorkItem
保持简单,例如here。
或者使用旧的 Obj-C 方式:
func searchBar(searchBar: UISearchBar, textDidChange searchText: String)
// to limit network activity, reload half a second after last key press.
NSObject.cancelPreviousPerformRequestsWithTarget(self, selector: "reload", object: nil)
self.performSelector("reload", withObject: nil, afterDelay: 0.5)
编辑:SWIFT 3 版本
func searchBar(searchBar: UISearchBar, textDidChange searchText: String)
// to limit network activity, reload half a second after last key press.
NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(self.reload), object: nil)
self.perform(#selector(self.reload), with: nil, afterDelay: 0.5)
@objc func reload()
print("Doing things")
【讨论】:
好答案!我只是对其进行了一点改进,您可以check it out :) 感谢@AhmadF,我正在考虑进行 SWIFT 4 更新。你做到了! :D 对于 Swift 4,使用DispatchWorkItem
,如上所述。它比选择器更优雅。【参考方案3】:
改进的 Swift 4+:
假设您已经符合UISearchBarDelegate
,这是VivienG's answer 的改进 Swift 4 版本:
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String)
NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(self.reload(_:)), object: searchBar)
perform(#selector(self.reload(_:)), with: searchBar, afterDelay: 0.75)
@objc func reload(_ searchBar: UISearchBar)
guard let query = searchBar.text, query.trimmingCharacters(in: .whitespaces) != "" else
print("nothing to search")
return
print(query)
实现cancelPreviousPerformRequests(withTarget:)的目的是为了防止搜索栏的每次变化都连续调用reload()
(不加的话,如果你输入“abc”,reload()
会根据基础被调用3次添加的字符数)。
改进是:在reload()
方法中有sender参数,即搜索栏;因此,通过将其声明为类中的全局属性,可以访问其文本(或其任何方法/属性)。
【讨论】:
对我很有帮助,用选择器中的搜索栏对象解析 我刚刚在 OBJC 中尝试过 - (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(validateText:) object:searchBar]; [self performSelector:@selector(validateText:) withObject:searchBar afterDelay:0.5]; 【参考方案4】:感谢this link,我找到了一种非常快速和干净的方法。与 Nirmit 的答案相比,它缺少“加载指示器”,但是它在代码行数方面胜出,并且不需要额外的控制。我首先将dispatch_cancelable_block.h
文件添加到我的项目中(来自this repo),然后定义了以下类变量:__block dispatch_cancelable_block_t searchBlock;
。
我的搜索代码现在看起来像这样:
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
if (searchBlock != nil)
//We cancel the currently scheduled block
cancel_block(searchBlock);
searchBlock = dispatch_after_delay(searchBlockDelay, ^
//We "enqueue" this block with a certain delay. It will be canceled if the user types faster than the delay, otherwise it will be executed after the specified delay
[self loadPlacesAutocompleteForInput:searchText];
);
注意事项:
loadPlacesAutocompleteForInput
是LPGoogleFunctions 库的一部分
searchBlockDelay
在@implementation
之外定义如下:
静态 CGFloat searchBlockDelay = 0.2;
【讨论】:
博客文章的链接对我来说似乎已死 @jeroen 你是对的:不幸的是,作者似乎从他的网站上删除了该博客。 GitHub 上引用该博客的存储库仍在运行,因此您可能需要在此处检查代码:github.com/SebastienThiebaud/dispatch_cancelable_block searchBlock 内部的代码永远不会执行。是否需要更多代码?【参考方案5】:快速破解如下:
- (void)textViewDidChange:(UITextView *)textView
static NSTimer *timer;
[timer invalidate];
timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(requestNewDataFromServer) userInfo:nil repeats:NO];
每次文本视图更改时,计时器都会失效,导致它不会触发。创建一个新的计时器并设置为在 1 秒后触发。搜索仅在用户停止输入 1 秒后更新。
【讨论】:
看起来我们有相同的方法,而且这个甚至不需要额外的代码。虽然requestNewDataFromServer
方法需要修改才能从userInfo
获取参数
是的,根据您的需要进行修改。概念是一样的。
由于在这种方法中从未触发计时器,我发现这里缺少一行: [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
@itinance 你是什么意思?使用代码中的方法创建计时器时,计时器已经在当前运行循环中。
这是一个快速而简洁的解决方案。您也可以在您的其他网络请求中使用它,例如在我的情况下,每次用户拖动他/她的地图时,我都会获取新数据。请注意,在 Swift 中,您需要通过调用 scheduledTimer...
来实例化计时器对象。【参考方案6】:
Swift 4 解决方案,以及一些通用的 cmets:
这些都是合理的方法,但是如果您想要示范性的自动搜索行为,您确实需要两个单独的计时器或调度。
理想的行为是 1) 定期触发自动搜索,但 2) 不会太频繁(因为服务器负载、蜂窝带宽以及可能导致 UI 卡顿),以及 3) 一旦有用户输入暂停。
您可以使用一个较长时间的计时器来实现此行为,该计时器在编辑开始后立即触发(我建议 2 秒)并且无论以后的活动如何都允许运行,再加上一个短期计时器(约 0.75 秒),即在每次更改时重置。任何一个计时器到期都会触发自动搜索并重置两个计时器。
最终的结果是,连续键入会在较长的几秒内产生一次自动搜索,但保证在短时间内会触发一次自动搜索。
您可以使用下面的 AutosearchTimer 类非常简单地实现此行为。使用方法如下:
// The closure specifies how to actually do the autosearch
lazy var timer = AutosearchTimer [weak self] in self?.performSearch()
// Just call activate() after all user activity
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String)
timer.activate()
func searchBarSearchButtonClicked(_ searchBar: UISearchBar)
performSearch()
func performSearch()
timer.cancel()
// Actual search procedure goes here...
AutosearchTimer 在释放时会自行处理清理工作,因此您无需在自己的代码中担心这一点。但是不要给计时器一个对 self 的强引用,否则你会创建一个引用循环。
下面的实现使用了计时器,但如果您愿意,您可以根据调度操作对其进行重铸。
// Manage two timers to implement a standard autosearch in the background.
// Firing happens after the short interval if there are no further activations.
// If there is an ongoing stream of activations, firing happens at least
// every long interval.
class AutosearchTimer
let shortInterval: TimeInterval
let longInterval: TimeInterval
let callback: () -> Void
var shortTimer: Timer?
var longTimer: Timer?
enum Const
// Auto-search at least this frequently while typing
static let longAutosearchDelay: TimeInterval = 2.0
// Trigger automatically after a pause of this length
static let shortAutosearchDelay: TimeInterval = 0.75
init(short: TimeInterval = Const.shortAutosearchDelay,
long: TimeInterval = Const.longAutosearchDelay,
callback: @escaping () -> Void)
shortInterval = short
longInterval = long
self.callback = callback
func activate()
shortTimer?.invalidate()
shortTimer = Timer.scheduledTimer(withTimeInterval: shortInterval, repeats: false)
[weak self] _ in self?.fire()
if longTimer == nil
longTimer = Timer.scheduledTimer(withTimeInterval: longInterval, repeats: false)
[weak self] _ in self?.fire()
func cancel()
shortTimer?.invalidate()
longTimer?.invalidate()
shortTimer = nil; longTimer = nil
private func fire()
cancel()
callback()
【讨论】:
【参考方案7】:请查看我在可可控件上找到的以下代码。他们正在异步发送请求以获取数据。可能他们正在从本地获取数据,但您可以使用远程 API 进行尝试。在后台线程中向远程 API 发送异步请求。请点击以下链接:
https://www.cocoacontrols.com/controls/jcautocompletingsearch
【讨论】:
嗨!我终于有时间看看你建议的控制。这绝对很有趣,我毫不怀疑许多人会从中受益。但是,我认为我从这篇博文中找到了一个更短(并且在我看来更简洁)的解决方案,这要归功于您的链接中的一些灵感:sebastienthiebaud.us/blog/ios/gcd/block/2014/04/09/… @maggix 您提供的链接现已过期。你能建议任何其他链接吗? 我正在更新此线程中的所有链接。使用下面我的答案中的一个 (github.com/SebastienThiebaud/dispatch_cancelable_block) 如果您使用的是 Google 地图,请查看此内容。这与 iOS 8 兼容并用 Objective-C 编写。 github.com/hkellaway/HNKGooglePlacesAutocomplete【参考方案8】:我们可以使用dispatch_source
+ (void)runBlock:(void (^)())block withIdentifier:(NSString *)identifier throttle:(CFTimeInterval)bufferTime
if (block == NULL || identifier == nil)
NSAssert(NO, @"Block or identifier must not be nil");
dispatch_source_t source = self.mappingsDictionary[identifier];
if (source != nil)
dispatch_source_cancel(source);
source = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
dispatch_source_set_timer(source, dispatch_time(DISPATCH_TIME_NOW, bufferTime * NSEC_PER_SEC), DISPATCH_TIME_FOREVER, 0);
dispatch_source_set_event_handler(source, ^
block();
dispatch_source_cancel(source);
[self.mappingsDictionary removeObjectForKey:identifier];
);
dispatch_resume(source);
self.mappingsDictionary[identifier] = source;
更多关于Throttling a block execution using GCD
如果您使用的是ReactiveCocoa,请考虑RACSignal
上的throttle
方法
这里是ThrottleHandler in Swift,你有兴趣
【讨论】:
我觉得github.com/SebastienThiebaud/dispatch_cancelable_block/blob/… 也很有用【参考方案9】:Swift 2.0 版本的 NSTimer 解决方案:
private var searchTimer: NSTimer?
func doMyFilter()
//perform filter here
func searchBar(searchBar: UISearchBar, textDidChange searchText: String)
if let searchTimer = searchTimer
searchTimer.invalidate()
searchTimer = NSTimer.scheduledTimerWithTimeInterval(0.5, target: self, selector: #selector(MySearchViewController.doMyFilter), userInfo: nil, repeats: false)
【讨论】:
【参考方案10】:您可以在 Swift 4.0 或更高版本中使用DispatchWorkItem
。这更容易,也很有意义。
我们可以在用户 0.25 秒内没有输入时执行 API 调用。
class SearchViewController: UIViewController, UISearchBarDelegate
// We keep track of the pending work item as a property
private var pendingRequestWorkItem: DispatchWorkItem?
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String)
// Cancel the currently pending item
pendingRequestWorkItem?.cancel()
// Wrap our request in a work item
let requestWorkItem = DispatchWorkItem [weak self] in
self?.resultsLoader.loadResults(forQuery: searchText)
// Save the new work item and execute it after 250 ms
pendingRequestWorkItem = requestWorkItem
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(250),
execute: requestWorkItem)
您可以从here阅读有关它的完整文章
【讨论】:
【参考方案11】: 免责声明:我是作者。如果您需要基于香草基础的节流功能, 如果您只想要一个线性 API,而不需要涉及响应式、组合、计时器、NSObject 取消和任何复杂的东西,
Throttler 是完成工作的正确工具。
您可以使用节流而不进行响应,如下所示:
import Throttler
for i in 1...1000
Throttler.go
print("throttle! > \(i)")
// throttle! > 1000
import UIKit
import Throttler
class ViewController: UIViewController
@IBOutlet var button: UIButton!
var index = 0
/********
Assuming your users will tap the button, and
request asyncronous network call 10 times(maybe more?) in a row within very short time nonstop.
*********/
@IBAction func click(_ sender: Any)
print("click1!")
Throttler.go
// Imaging this is a time-consuming and resource-heavy task that takes an unknown amount of time!
let url = URL(string: "https://jsonplaceholder.typicode.com/todos/1")!
let task = URLSession.shared.dataTask(with: url) (data, response, error) in
guard let data = data else return
self.index += 1
print("click1 : \(self.index) : \(String(data: data, encoding: .utf8)!)")
override func viewDidLoad()
super.viewDidLoad()
// Do any additional setup after loading the view.
click1!
click1!
click1!
click1!
click1!
click1!
click1!
click1!
click1!
click1!
2021-02-20 23:16:50.255273-0500 iOSThrottleTest[24776:813744]
click1 : 1 :
"userId": 1,
"id": 1,
"title": "delectus aut autem",
"completed": false
如果你想要一些特定的延迟秒数:
import Throttler
for i in 1...1000
Throttler.go(delay:1.5)
print("throttle! > \(i)")
// throttle! > 1000
【讨论】:
【参考方案12】:斯威夫特 5.0
基于GSnyder
响应
//
// AutoSearchManager.swift
// BTGBankingCommons
//
// Created by Matheus Gois on 01/10/21.
//
import Foundation
/// Manage two timers to implement a standard auto search in the background.
/// Firing happens after the short interval if there are no further activations.
/// If there is an ongoing stream of activations, firing happens at least every long interval.
public class AutoSearchManager
// MARK: - Properties
private let shortInterval: TimeInterval
private let longInterval: TimeInterval
private let callback: (Any?) -> Void
private var shortTimer: Timer?
private var longTimer: Timer?
// MARK: - Lifecycle
public init(
short: TimeInterval = Constants.shortAutoSearchDelay,
long: TimeInterval = Constants.longAutoSearchDelay,
callback: @escaping (Any?) -> Void
)
shortInterval = short
longInterval = long
self.callback = callback
// MARK: - Methods
public func activate(_ object: Any? = nil)
shortTimer?.invalidate()
shortTimer = Timer.scheduledTimer(
withTimeInterval: shortInterval,
repeats: false
) [weak self] _ in self?.fire(object)
if longTimer == nil
longTimer = Timer.scheduledTimer(
withTimeInterval: longInterval,
repeats: false
) [weak self] _ in self?.fire(object)
public func cancel()
shortTimer?.invalidate()
longTimer?.invalidate()
shortTimer = nil
longTimer = nil
// MARK: - Private methods
private func fire(_ object: Any? = nil)
cancel()
callback(object)
// MARK: - Constants
extension AutoSearchManager
public enum Constants
/// Auto-search at least this frequently while typing
public static let longAutoSearchDelay: TimeInterval = 2.0
/// Trigger automatically after a pause of this length
public static let shortAutoSearchDelay: TimeInterval = 0.75
【讨论】:
以上是关于如何在 iOS UISearchBar 中限制搜索(基于打字速度)?的主要内容,如果未能解决你的问题,请参考以下文章
ios 7中yelp和foursquare如何将UISearchBar中的搜索图标保持在左侧
如何在 iOS 中为 UISearchBar 设置最近的搜索历史下拉列表
如何在 iOS 10 的目标 c 中使用 uisearchbar?