iOS UISearchBarで(入力速度に基づいて)検索を調整する方法は?


80

ローカルCoreDataとリモートAPIの両方からの検索結果を表示するために使用されるUISearchDisplayControllerのUISearchBar部分があります。私が達成したいのは、リモートAPIでの検索の「遅延」です。現在、ユーザーが入力した文字ごとに、リクエストが送信されます。ただし、ユーザーが特に速く入力する場合、多くのリクエストを送信することは意味がありません。入力が停止するまで待つと役立ちます。それを達成する方法はありますか?

ドキュメントを読むと、ユーザーが明示的に検索をタップするまで待つことをお勧めしますが、私の場合は理想的ではありません。

パフォーマンスの問題。検索操作を非常に迅速に実行できる場合は、デリゲートオブジェクトにsearchBar:textDidChange:メソッドを実装することで、ユーザーが入力しているときに検索結果を更新できます。ただし、検索操作に時間がかかる場合は、ユーザーが[検索]ボタンをタップするまで待ってから、searchBarSearchButtonClicked:メソッドで検索を開始する必要があります。メインスレッドのブロックを回避するために、常にバックグラウンドスレッドで検索操作を実行してください。これにより、検索の実行中にアプリがユーザーに応答し続け、ユーザーエクスペリエンスが向上します。

APIに多くのリクエストを送信することは、ローカルパフォーマンスの問題ではなく、リモートサーバーでのリクエスト率が高すぎることを回避することだけです。

ありがとう


1
タイトルが正しいかわかりません。あなたが求めているのは「スロットル」ではなく「デバウンス」と呼ばれています。
V_tredue

回答:


132

この魔法を試してください:

- (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メソッドについて知りませんでした!
jesses.co.tt 2015

どういたしまして!それは素晴らしいパターンであり、あらゆる種類のものに使用できます。
malhal 2015

とても便利です!これが本当のブードゥーです
Matteo Pacini 2015年

2
「リロード」について...さらに数秒考えなければなりませんでした...これは、ユーザーが0.5秒間入力を停止した後、実際に実行したいことを実行するローカルメソッドを指します。このメソッドは、searchExecuteのように、好きなように呼び出すことができます。ありがとう!
blalond 2016

これは私には機能しません...文字が変更されるたびに「リロード」機能を実行し続けます
Andrey

52

Swift 4以降でこれが必要な人のために:

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

編集:SWIFT3バージョン

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

1
いい答えです!私はそれに少し改善を加えました、あなたはそれをチェックすることができます:)
Ahmad F

@AhmadFに感謝します、私はSWIFT4アップデートを行うことを考えていました。できたね!:D
VivienG 2017

1
Swift 4の場合、DispatchWorkItem上記で最初に提案したように使用します。セレクターよりもエレガントに動作します。
テフィ

21

改善されたSwift4:

すでに準拠していると仮定するとUISearchBarDelegate、これはVivienGの回答の改良されたSwift4バージョンです。

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()メソッド検索バーで、送信者のパラメータを持っています。したがって、そのテキスト(またはそのメソッド/プロパティのいずれか)にアクセスするには、クラス内のグローバルプロパティとして宣言することでアクセスできます。


セレクターの検索バーのオブジェクトを解析することは、私にとって本当に役に立ちます
HariNarayanan20年

OBJCで試しました-(void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText {[NSObject cancelPreviousPerformRequestsWithTarget:selfselector:@selector(validateText :) object:searchBar]; [self performSelector:@selector(validateText :) withObject:searchBar afterDelay:0.5]; }
ハリナラヤナン

18

このリンクのおかげで、私は非常に迅速でクリーンなアプローチを見つけました。Nirmitの回答と比較すると、「読み込みインジケーター」がありませんが、コードの行数の点で勝ち、追加のコントロールは必要ありません。最初にdispatch_cancelable_block.hファイルを(このリポジトリから)プロジェクトに追加し、次に次のクラス変数を定義しました__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:の外で次のように定義されます。

    静的CGFloatsearchBlockDelay = 0.2;


1
ブログ投稿へのリンクは私には死んでいるように見えます
jeroen 2015年

1
@jeroenその通りです。残念ながら、作者が自分のWebサイトからブログを削除したようです。そのブログを参照しているGitHubのリポジトリはまだ稼働している
maggix

searchBlock内のコードは実行されません。もっとコードが必要ですか?
旅程2015年

12

簡単なハックは次のようになります。

- (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秒間入力を停止した後にのみ更新されます。


同じアプローチを採用したようですが、これには追加のコードも必要ありません。requestNewDataFromServeruserInfo
maggix

はい、必要に応じて変更してください。コンセプトは同じです。
duci9y 2014

3
このアプローチではタイマーが起動されないため、ここで1行が欠落していることがわかりました。[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
旅程2015年

@itinanceどういう意味ですか?コード内のメソッドを使用してタイマーを作成すると、タイマーはすでに現在の実行ループに入っています。
duci9y 2015年

これは迅速できちんとした解決策です。これを他のネットワークリクエストでも使用できます。私の状況では、ユーザーがマップをドラッグするたびに新しいデータをフェッチします。Swiftでは、を呼び出してタイマーオブジェクトをインスタンス化する必要があることに注意してくださいscheduledTimer...
グレン・ポサダス

5

Swift 4ソリューションといくつかの一般的なコメント:

これらはすべて合理的なアプローチですが、模範的な自動検索動作が必要な場合は、2つの別々のタイマーまたはディスパッチが実際に必要です。

理想的な動作は、1)自動検索が定期的にトリガーされるが、2)あまり頻繁ではない(サーバーの負荷、セルラー帯域幅、およびUIスタッターを引き起こす可能性があるため)、3)一時停止があるとすぐにトリガーされることです。ユーザーの入力。

この動作は、編集が開始されるとすぐにトリガーされ(2秒をお勧めします)、後のアクティビティに関係なく実行できる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()
    }

}

3

私がココアコントロールで見つけた次のコードを参照してください。彼らはデータをフェッチするために非同期でリクエストを送信しています。ローカルからデータを取得している可能性がありますが、リモートAPIで試すことができます。バックグラウンドスレッドでリモートAPIに非同期リクエストを送信します。以下のリンクをたどってください:

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


こんにちは!私はついにあなたの提案されたコントロールを見る時間がありました。それは間違いなく興味深いものであり、多くの人がそれから恩恵を受けることは間違いありません。しかし、私は、あなたのリンクからいくつかのインスピレーションのおかげで、このブログの記事から短い(と、私の意見では、クリーンな)解決策を見つけたと思う:sebastienthiebaud.us/blog/ios/gcd/block/2014/04/09/...を
maggix 2014

@maggix指定したリンクの有効期限が切れました。他のリンクを提案できますか。
Nirmit Dagly 2015

このスレッドのすべてのリンクを更新しています。(下記の私の答えに1を使用しgithub.com/SebastienThiebaud/dispatch_cancelable_block
maggix

グーグルマップを使用している場合は、これも見てください。これはiOS8と互換性があり、objective-cで記述されています。github.com/hkellaway/HNKGooglePlacesAutocomplete
Nirmit Dagly 2015

3

使用できます 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;
}

GCDを使用したブロック実行のスロットリングの詳細

あなたが使用している場合ReactiveCocoaを、検討するthrottle上での方法をRACSignal

これがSwiftのThrottleHandlerです。



3

NSTimerソリューションのSwift2.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)
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.