【问题标题】:How to throttle search (based on typing speed) in iOS UISearchBar?如何在 iOS UISearchBar 中限制搜索(基于打字速度)?
【发布时间】:2014-06-20 14:50:05
【问题描述】:

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

阅读documentation 建议等到用户明确点击搜索,但我认为这并不理想。

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

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

谢谢

【问题讨论】:

  • 我不确定标题是否正确。您要求的是“去抖动”而不是“节流”。

标签: ios objective-c swift search


【解决方案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]; 
    });
}

注意事项:

  • loadPlacesAutocompleteForInputLPGoogleFunctions 库的一部分
  • 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

    【讨论】:

    【解决方案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,你有兴趣

    【解决方案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
              }
          }
          
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2011-07-29
            • 2021-09-18
            • 1970-01-01
            • 2011-12-09
            • 2014-01-14
            • 1970-01-01
            • 2010-09-17
            相关资源
            最近更新 更多