ローカルのCoreDataとリモートAPIの両方からの検索結果を表示するために使用されるUISearchDisplayControllerのUISearchBar部分があります。私が達成したいのは、リモートAPIでの検索の「遅延」です。現在、ユーザーが入力した文字ごとに、リクエストが送信されます。ただし、ユーザーが特に高速で入力する場合、多くの要求を送信することは意味がありません。入力を停止するまで待機するのに役立ちます。それを達成する方法はありますか?
ドキュメント を読むと、ユーザーが検索を明示的にタップするまで待つことをお勧めしますが、私の場合は理想的ではありません。
パフォーマンスの問題。検索操作を非常に迅速に実行できる場合、デリゲートオブジェクトでsearchBar:textDidChange:メソッドを実装することにより、ユーザーが入力しているときに検索結果を更新することができます。ただし、検索操作に時間がかかる場合は、ユーザーが[検索]ボタンをタップするまで待ってから、searchBarSearchButtonClicked:メソッドで検索を開始する必要があります。メインスレッドがブロックされないように、常にバックグラウンドスレッドで検索操作を実行します。これにより、検索の実行中にアプリがユーザーに応答しやすくなり、ユーザーエクスペリエンスが向上します。
APIに多くのリクエストを送信することは、ローカルパフォーマンスの問題ではなく、リモートサーバーでの高すぎるリクエストレートの回避のみです。
ありがとう
このリンク のおかげで、非常に迅速でクリーンなアプローチが見つかりました。 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;
この魔法を試してください:
-(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と呼ばれるメソッドを呼び出していますが、好きなメソッドを呼び出すことができます。
Swift 4以降でこれを必要とする人々のために:
here のような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")
}
すでに UISearchBarDelegate
に準拠していると仮定すると、これはimprovedSwift 4バージョンの4 VivienGの答え :
_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回呼び出されます)。
improvementは次のとおりです。in reload()
メソッドには、検索バーであるsenderパラメーターがあります。したがって、そのテキストまたはそのメソッド/プロパティのいずれかにアクセスするには、クラス内のグローバルプロパティとして宣言することでアクセスできます。
簡単なハックは次のようになります。
- (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秒間停止した後にのみ更新されます。
Swift 4ソリューション、およびいくつかの一般的なコメント:
これらはすべて合理的なアプローチですが、模範的な自動検索動作が必要な場合は、2つの別個のタイマーまたはディスパッチが本当に必要です。
理想的な動作は、1)定期的に自動検索がトリガーされますが、2)あまり頻繁ではない(サーバー負荷、セルラー帯域幅、UIのスタッターが発生する可能性があるため)、3)で一時停止するとすぐにトリガーされますユーザーの入力。
編集を開始するとすぐにトリガーされ、後のアクティビティに関係なく実行できる1つの長期タイマーと、毎回リセットされる1つの短期タイマー(〜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()
}
}
NSTimerソリューションのSwift 2.0バージョン:
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)
}
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;
}
ReactiveCocoa を使用している場合は、throttle
のRACSignal
メソッドを検討してください
興味があるのは SwiftのThrottleHandler です
ココアコントロールで見つけた次のコードを参照してください。データをフェッチするために非同期でリクエストを送信しています。ローカルからデータを取得している可能性がありますが、リモートAPIで試すことができます。バックグラウンドスレッドでリモートAPIに非同期要求を送信します。以下のリンクに従ってください:
https://www.cocoacontrols.com/controls/jcautocompletingsearch