我有UISearchDisplayController的UISearchBar部分,用于显示本地CoreData和远程API的搜索结果。 我想要实现的是"延迟"远程API上的搜索。目前,对于用户键入的每个字符,发送请求。但是如果用户输入特别快,那么发送许多请求就没有意义:等到他停止输入会有所帮助。 有没有办法实现这一目标?
阅读documentation建议等到用户明确点击搜索,但在我的情况下,我发现它并不理想。
性能问题。如果可以进行搜索操作 迅速地,可以像用户那样更新搜索结果 通过在。上实现searchBar:textDidChange:方法来键入 委托对象。但是,如果搜索操作需要更多时间,那么 应该等到用户在开始之前点击“搜索”按钮 在searchBarSearchButtonClicked:方法中搜索。总是表演 搜索操作后台线程以避免阻塞主要 线。这可以使您的应用在搜索时对用户做出响应 运行并提供更好的用户体验。
向API发送许多请求不是本地性能的问题,而只是避免远程服务器上的请求率过高。
由于
答案 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以后
中需要此功能的用户:使用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)
}
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];
});
}
注意:
loadPlacesAutocompleteForInput
是LPGoogleFunctions库 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)
假设您已经符合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
阅读有关它的完整文章