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

时间:2014-06-20 14:50:05

标签: ios objective-c swift search

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

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

  

性能问题。如果可以进行搜索操作   迅速地,可以像用户那样更新搜索结果   通过在。上实现searchBar:textDidChange:方法来键入   委托对象。但是,如果搜索操作需要更多时间,那么   应该等到用户在开始之前点击“搜索”按钮   在searchBarSearchButtonClicked:方法中搜索。总是表演   搜索操作后台线程以避免阻塞主要   线。这可以使您的应用在搜索时对用户做出响应   运行并提供更好的用户体验。

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

由于

11 个答案:

答案 0 :(得分:116)

试试这个魔法:

-(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的方法,但您可以调用任何您喜欢的方法!

答案 1 :(得分:41)

对于在 Swift 4以后

中需要此功能的用户:

使用DispatchWorkItemhere保持简单。

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

答案 2 :(得分:15)

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

注意:

  • loadPlacesAutocompleteForInputLPGoogleFunctions
  • 的一部分
  • searchBlockDelay@implementation

    之外定义如下

    static CGFloat searchBlockDelay = 0.2;

答案 3 :(得分:10)

快速黑客就是这样:

- (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秒后才会更新搜索。

答案 4 :(得分:8)

改进了Swift 4:

假设您已经符合UISearchBarDelegate,这是改进的 Swift 4版VivienG's answer

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",{ {1}}将根据添加的字符数被调用三次。

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

答案 5 :(得分: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)
}

答案 6 :(得分:3)

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

}

答案 7 :(得分:2)

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

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

答案 8 :(得分:2)

我们可以使用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,请考虑throttle

上的RACSignal方法

以下是您感兴趣的ThrottleHandler in Swift

答案 9 :(得分:0)

  • 免责声明:我是作者。

如果您需要基于 vanilla Foundation 的节流功能,
如果你只想要一个线性 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

答案 10 :(得分:-1)

您可以在 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

阅读有关它的完整文章