web-dev-qa-db-ja.com

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

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

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

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

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

ありがとう

64
maggix

このリンク のおかげで、非常に迅速でクリーンなアプローチが見つかりました。 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;

15
maggix

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

-(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と呼ばれるメソッドを呼び出していますが、好きなメソッドを呼び出すことができます。

118
malhal

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

改善されたSwift 4:

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

10
Ahmad F

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

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

10
duci9y

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

}
3
GSnyder

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)
}
3
William T.

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 を使用している場合は、throttleRACSignalメソッドを検討してください

興味があるのは SwiftのThrottleHandler です

2
onmyway133

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

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

2
Nirmit Dagly