如何在iOS UISearchBar中限制搜索(基于键入速度)?


80

我有UISearchDisplayController的UISearchBar部分,该部分用于显示来自本地CoreData和远程API的搜索结果。我要实现的是在远程API上进行搜索的“延迟”。当前,对于用户键入的每个字符,都会发送一个请求。但是,如果用户输入速度特别快,那么发送许多请求就没有意义了:等到他停止输入后,这才有帮助。有办法实现吗?

阅读文档建议您等到用户明确点击搜索后,但就我而言,它并不理想。

性能问题。如果可以非常快速地执行搜索操作,则可以通过在委托对象上实现searchBar:textDidChange:方法来更新用户输入时的搜索结果。但是,如果搜索操作花费更多时间,则应等到用户点按“搜索”按钮,然后才能在searchBarSearchButtonClicked:方法中开始搜索。始终在后台线程中执行搜索操作,以避免阻塞主线程。这样可以在搜索运行时使您的应用对用户保持响应,并提供更好的用户体验。

向API发送许多请求不是本地性能的问题,而仅仅是避免远程服务器上的请求率过高。

谢谢


1
我不确定标题是否正确。您要的是“防抖动”而不是“油门”。
Vitto_tredue

Answers:


130

试试这个魔术:

- (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];
}

迅捷版:

 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方法!
jesses.co.tt 2015年

别客气!这是一个很棒的模式,可以用于各种各样的事情。
malhal 2015年

太有用了!这才是真正的巫术
利玛窦帕齐尼

2
关于“重新加载”……我不得不多花几秒钟的时间……这指的是本地方法,该方法实际上将在用户停止键入0.5秒后执行您要执行的操作。可以根据需要调用该方法,例如searchExecute。谢谢!
2016年

这对我不起作用...每次更改字母时,它都会继续运行“重新加载”功能
Andrey

52

对于需要在Swift 4及更高版本中使用此功能的人:

保持简单用DispatchWorkItem这里


或使用旧的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)
}
func reload() {
    print("Doing things")
}

1
好答案!我只是对其进行了一些改进,您可以查看一下:)
Ahmad F

谢谢@AhmadF,我正在考虑进行SWIFT 4更新。你做到了!:D
VivienG

1
对于Swift 4,请DispatchWorkItem按照上述第一个建议使用。它比选择器优雅地工作。
特菲,

21

改进的Swift 4:

假设您已经遵守了UISearchBarDelegate,这是VivienG的答案改进的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()方法具有发送器参数是搜索栏; 因此,通过在类中将其声明为全局属性,可以访问其文本-或其任何方法/属性。


解析选择器中搜索栏的对象
Hari Narayanan,

我刚刚在OBJC中尝试过-(void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText {[NSObject cancelPreviousPerformRequestsWithTarget:self选择器:@selector(validateText :) object:searchBar]; [具有对象:searchBar afterDelay:0.5的[self performSelector:@selector(validateText :)]]; }
Hari Narayanan

18

通过此链接,我找到了一种非常快捷,干净的方法。与Nirmit的答案相比,它缺少“加载指示器”,但是在代码行数方面胜出,并且不需要其他控件。我首先将该dispatch_cancelable_block.h文件添加到了我的项目(来自此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;


1
博客文章的链接对我似乎已经死了
jeroen 2015年

1
@jeroen,您是对的:很遗憾,作者似乎从其网站上删除了该博客。GitHub上指向该博客的存储库仍在运行中,因此您可能需要在此处检查代码:github.com/SebastienThiebaud/dispatch_cancelable_block
maggix 2015年

searchBlock内部的代码永远不会执行。是否需要更多代码?
itinance 2015年

12

快速修改如下:

- (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
maggix 2014年

是的,根据您的需要进行修改。概念是相同的。
duci9y 2014年

3
由于从不使用这种方法触发计时器,因此我发现此处缺少一行:[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
itinance

@itinance是什么意思?使用代码中的方法创建计时器时,该计时器已在当前运行循环中。
duci9y

这是一个快速而整洁的解决方案。您也可以在其他网络请求中使用此功能,例如在我的情况下,每次用户拖动其地图时我都会获取新数据。请注意,在Swift中,您需要通过调用实例化计时器对象scheduledTimer...
Glenn Posadas

5

Swift 4解决方案,外加一些一般性注释:

这些都是合理的方法,但是如果您想要示范性的自动搜索行为,则确实需要两个单独的计时器或调度。

理想的行为是: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在释放时会处理自己的清理操作,因此您无需在自己的代码中担心这一点。但是,不要给计时器强烈的自我参考,否则您将创建一个参考周期。

下面的实现使用计时器,但是您可以根据需要在调度操作方面重铸它。

// 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()
    }

}

3

请查看我在可可粉控件上找到的以下代码。他们正在异步发送请求以获取数据。可能是他们从本地获取数据,但是您可以使用远程API尝试。在后台线程中对远程API发送异步请求。请点击以下链接:

https://www.cocoacontrols.com/controls/jcautocompletingsearch


嗨!我终于有时间查看您建议的控件。这绝对是有趣的,毫无疑问,许多人将从中受益。不过,我想我找到了一个更短的(并且,在我看来,更清洁)从这篇博客的解决方案,这要归功于从链接一些启示:sebastienthiebaud.us/blog/ios/gcd/block/2014/04/09/...
maggix 2014年

您提供的链接@maggix现在已过期。您能否建议其他任何链接。
Nirmit Dagly

我正在更新此线程中的所有链接。使用下(我的回答的一个github.com/SebastienThiebaud/dispatch_cancelable_block
maggix

如果您使用的是Google地图,请同时查看。这与iOS 8兼容并用Objective-C编写。github.com/hkellaway/HNKGooglePlacesAutocomplete
Nirmit Dagly 2015年

3

我们可以用 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;
}

有关使用GCD限制块执行的更多信息

如果您使用的是ReactiveCocoa,请考虑以下throttle方法RACSignal

这是您感兴趣的Swift中的ThrottleHandler



3

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)
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.