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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/24330056/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-31 01:00:45  来源:igfitidea点击:

How to throttle search (based on typing speed) in iOS UISearchBar?

iosobjective-cswiftsearch

提问by maggix

I have a UISearchBar part of a UISearchDisplayController that is used to display search results from both local CoreData and remote API. What I want to achieve is the "delaying" of the search on the remote API. Currently, for each character typed by the user, a request is sent. But if the user types particularly fast, it does not make sense to send many requests: it would help to wait until he has stopped typing. Is there a way to achieve that?

我有一个 UISearchDisplayController 的 UISearchBar 部分,用于显示来自本地 CoreData 和远程 API 的搜索结果。我想要实现的是远程API上搜索的“延迟”。目前,对于用户输入的每个字符,都会发送一个请求。但是如果用户打字特别快,发送很多请求是没有意义的:等到他停止打字会有所帮助。有没有办法实现这一目标?

Reading the documentationsuggests to wait until the users explicitly taps on search, but I don't find it ideal in my case.

阅读文档建议等到用户明确点击搜索,但在我的情况下我觉得它并不理想。

Performance issues. If search operations can be carried out very rapidly, it is possible to update the search results as the user is typing by implementing the searchBar:textDidChange: method on the delegate object. However, if a search operation takes more time, you should wait until the user taps the Search button before beginning the search in the searchBarSearchButtonClicked: method. Always perform search operations a background thread to avoid blocking the main thread. This keeps your app responsive to the user while the search is running and provides a better user experience.

性能问题。如果可以非常快速地执行搜索操作,则可以通过在委托对象上实现 searchBar:textDidChange: 方法,在用户键入时更新搜索结果。但是,如果搜索操作需要更多时间,则应等待用户点击搜索按钮,然后再在 searchBarSearchButtonClicked: 方法中开始搜索。总是在后台线程中执行搜索操作以避免阻塞主线程。这可以让您的应用在搜索运行时对用户做出响应,并提供更好的用户体验。

Sending many requests to the API is not a problem of local performance but only of avoiding too high request rate on the remote server.

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

Thanks

谢谢

采纳答案by maggix

Thanks to this link, I found a very quick and clean approach. Compared to Nirmit's answer it lacks the "loading indicator", however it wins in terms of number of lines of code and does not require additional controls. I first added the dispatch_cancelable_block.hfile to my project (from this repo), then defined the following class variable: __block dispatch_cancelable_block_t searchBlock;.

感谢这个链接,我找到了一个非常快速和干净的方法。与 Nirmit 的答案相比,它缺少“加载指示器”,但是它在代码行数方面胜出,并且不需要额外的控制。我首先将dispatch_cancelable_block.h文件添加到我的项目中(来自这个 repo),然后定义了以下类变量:__block dispatch_cancelable_block_t searchBlock;.

My search code now looks like this:

我的搜索代码现在看起来像这样:

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

Notes:

笔记:

  • The loadPlacesAutocompleteForInputis part of the LPGoogleFunctionslibrary
  • searchBlockDelayis defined as follows outside of the @implementation:

    static CGFloat searchBlockDelay = 0.2;

  • loadPlacesAutocompleteForInput是金LPGoogleFunctions
  • searchBlockDelay在 之外定义如下@implementation

    静态 CGFloat searchBlockDelay = 0.2;

回答by malhal

Try this magic:

试试这个魔法:

-(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 version:

迅捷版:

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

Note this example calls a method called reload but you can make it call whatever method you like!

注意这个例子调用了一个叫做 reload 的方法,但是你可以让它调用任何你喜欢的方法!

回答by VivienG

For people who need this in Swift 4 onwards:

对于在Swift 4 以后需要这个的人:

Keep it simple with a DispatchWorkItemlike here.

保持简单用DispatchWorkItem这里



or use the old Obj-C way:

或使用旧的 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)
}

EDIT: SWIFT 3Version

编辑: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")
}

回答by Ahmad F

Improved Swift 4:

改进的 Swift 4:

Assuming that you are already conforming to UISearchBarDelegate, this is an improvedSwift 4 version of VivienG's answer:

假设您已经符合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)
}

The purpose of implementing cancelPreviousPerformRequests(withTarget:)is to prevent the continuous calling to the reload()for each change to the search bar (without adding it, if you typed "abc", reload()will be called three times based on the number of the added characters).

实现cancelPreviousPerformRequests(withTarget:)的目的是为了防止对reload()搜索栏的每次更改都不断调用for(不添加,如果你输入“abc”,reload()会根据添加的字符数调用3次) .

The improvementis: in reload()method has the sender parameter which is the search bar; Thus accessing its text -or any of its method/properties- would be accessible with declaring it as a global property in the class.

改进是:在reload()方法具有发送器参数是搜索栏; 因此,可以通过将其声明为类中的全局属性来访问其文本 - 或其任何方法/属性。

回答by duci9y

A quick hack would be like so:

一个快速的 hack 是这样的:

- (void)textViewDidChange:(UITextView *)textView
{
    static NSTimer *timer;
    [timer invalidate];
    timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(requestNewDataFromServer) userInfo:nil repeats:NO];
}

Every time the text view changes, the timer is invalidated, causing it not to fire. A new timer is created and set to fire after 1 second. The search is only updated after the user stops typing for 1 second.

每次文本视图更改时,计时器都会失效,导致它不会触发。创建一个新的计时器并设置为在 1 秒后触发。搜索仅在用户停止输入 1 秒后更新。

回答by GSnyder

Swift 4 solution, plus some general comments:

Swift 4 解决方案,以及一些一般性评论:

These are all reasonable approaches, but if you want exemplary autosearch behavior, you really need two separate timers or dispatches.

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

The ideal behavior is that 1) autosearch is triggered periodically, but 2) not too frequently (because of server load, cellular bandwidth, and the potential to cause UI stutters), and 3) it triggers rapidly as soon as there is a pause in the user's typing.

理想的行为是 1) 自动搜索会定期触发,但 2) 不会太频繁(因为服务器负载、蜂窝带宽和可能导致 UI 卡顿的可能性),以及 3) 一旦出现暂停,它就会迅速触发用户的输入。

You can achieve this behavior with one longer-term timer that triggers as soon as editing begins (I suggest 2 seconds) and is allowed to run regardless of later activity, plus one short-term timer (~0.75 seconds) that is reset on every change. The expiration of either timer triggers autosearch and resets both timers.

您可以使用一个长期计时器来实现此行为,该计时器在编辑开始时立即触发(我建议 2 秒)并且无论以后的活动如何都允许运行,再加上一个短期计时器(约 0.75 秒),该计时器在每次编辑时重置改变。任一计时器到期都会触发自动搜索并重置两个计时器。

The net effect is that continuous typing yields autosearches every long-period seconds, but a pause is guaranteed to trigger an autosearch within short-period seconds.

最终效果是连续打字会在每长时间秒内产生自动搜索,但保证暂停会在短时间秒内触发自动搜索。

You can implement this behavior very simply with the AutosearchTimer class below. Here's how to use it:

您可以使用下面的 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...
}

The AutosearchTimer handles its own cleanup when freed, so there's no need to worry about that in your own code. But don't give the timer a strong reference to self or you'll create a reference cycle.

AutosearchTimer 在被释放时会处理自己的清理工作,因此无需在您自己的代码中担心这一点。但是不要给计时器一个强引用 self 否则你会创建一个引用循环。

The implementation below uses timers, but you can recast it in terms of dispatch operations if you prefer.

下面的实现使用计时器,但如果您愿意,您可以根据调度操作重新转换它。

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

}

回答by Nirmit Dagly

Please see the following code which i've found on cocoa controls. They are sending request asynchronously to fetch the data. May be they are getting data from local but you can try it with the remote API. Send async request on remote API in background thread. Follow below link:

请参阅我在可可控件上找到的以下代码。他们异步发送请求以获取数据。可能是他们从本地获取数据,但您可以使用远程 API 进行尝试。在后台线程中在远程 API 上发送异步请求。按照以下链接:

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

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

回答by William T.

Swift 2.0 version of the NSTimer solution:

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 onmyway133

We can use dispatch_source

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

More on Throttling a block execution using GCD

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

If you're using ReactiveCocoa, consider throttlemethod on RACSignal

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

Here is ThrottleHandler in Swiftin you're interested

这是你感兴趣的Swift中的ThrottleHandler