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

천만에요! 그것은 훌륭한 패턴이며 모든 종류의 것에 사용될 수 있습니다.
malhal

매우 유용합니다! 이것은 진짜 부두입니다
마테오의 복잡한 배열

2
"다시로드"에 관해서는 ... 몇 초 더 생각해야했는데 ... 이는 사용자가 0.5 초 동안 입력을 중지 한 후 원하는 작업을 실제로 수행하는 로컬 방법을 의미합니다. 이 메서드는 searchExecute와 같이 원하는대로 호출 할 수 있습니다. 감사!
blalond aug

이것은 나를 위해 작동하지 않습니다 ... 그것은 문자가 변경 될 때마다 "다시로드"기능을 계속 실행합니다
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)
}

편집 : 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")
}

1
좋은 대답입니다! 난 그냥 당신이 수, 거기에 약간의 개선을 추가 밖으로 그것을 확인 :
아마드 F를

@AhmadF 감사합니다. SWIFT 4 업데이트를 할 생각이었습니다. 훌륭해! : D
VivienG

1
Swift 4의 경우 DispatchWorkItem위에서 제안한대로 사용하십시오. 선택자보다 우아하게 작동합니다.
Teffi 2019

21

향상된 Swift 4 :

이미 준수하고 있다고 가정하면 UISearchBarDelegate이것은 VivienG의 답변의 향상된 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()하면 추가 된 문자 수에 따라 세 번 호출됩니다). .

개선 이다의 reload()방법은, 검색 창 인 송신기 매개 변수를 갖는다; 따라서 텍스트 또는 메서드 / 속성에 액세스하는 것은 클래스에서 전역 속성으로 선언하여 액세스 할 수 있습니다.


그것의 나를 위해 정말 도움이 ,, 선택에서 검색 바의 개체와 구문 분석
하리 나라 야난

방금 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]; }
Hari Narayanan

18

이 링크 덕분에 매우 빠르고 깔끔한 접근 방식을 찾았습니다. Nirmit의 답변에 비해 "로딩 표시기"가 없지만 코드 줄 수 측면에서 이기고 추가 제어가 필요하지 않습니다. 먼저 dispatch_cancelable_block.h파일을 내 프로젝트 ( 이 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.

    정적 CGFloat searchBlockDelay = 0.2;


1
블로그 게시물에 대한 링크가 나에게 죽은 것처럼 보입니다
jeroen 2015 년

1
@jeroen 맞아요 : 안타깝게도 작성자가 웹 사이트에서 블로그를 삭제 한 것 같습니다. 해당 블로그를 참조한 GitHub의 저장소가 아직 작동 중이므로
maggix

searchBlock 내부의 코드는 실행되지 않습니다. 더 많은 코드가 필요합니까?
여행 일정

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 초 동안 입력을 중지 한 후에 만 ​​업데이트됩니다.


우리가 같은 접근 방식을 가지고있는 것처럼 보이며 추가 코드도 필요하지 않습니다. requestNewDataFromServer매개 변수를 가져 오기 위해 방법을 수정해야 하지만userInfo
maggix

네, 필요에 따라 수정하십시오. 개념은 동일합니다.
duci9y

3
이 접근 방식에서는 타이머가 실행되지 않기 때문에 여기에 한 줄이 없다는 것을 알아 냈습니다. [[NSRunLoop mainRunLoop] addTimer : timer forMode : NSDefaultRunLoopMode];
itinance

@itinance 무슨 뜻이야? 코드의 메서드로 타이머를 만들 때 타이머는 이미 현재 실행 루프에 있습니다.
duci9y

이것은 빠르고 깔끔한 솔루션입니다. 제 상황에서 사용자가지도를 끌 때마다 새 데이터를 가져 오는 것과 같이 다른 네트워크 요청에서도이를 사용할 수 있습니다. Swift에서는를 호출하여 타이머 객체를 인스턴스화하고 싶을 것 scheduledTimer...입니다.
글렌 포사다

5

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

}

3

코코아 컨트롤에서 찾은 다음 코드를 참조하십시오. 그들은 데이터를 가져 오기 위해 비동기 적으로 요청을 보내고 있습니다. 로컬에서 데이터를 가져올 수 있지만 원격 API로 시도 할 수 있습니다. 백그라운드 스레드에서 원격 API에 비동기 요청을 보냅니다. 아래 링크를 따르십시오.

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


안녕! 마침내 당신이 제안한 컨트롤을 살펴볼 시간이 생겼습니다. 확실히 흥미롭고 많은 사람들이 혜택을 볼 것입니다. 그러나 귀하의 링크에서 영감을 얻은 덕분에이 블로그 게시물에서 더 짧은 솔루션을 찾은 것
maggix

@maggix 귀하가 제공 한 링크는 이제 만료되었습니다. 다른 링크를 제안 해 주시겠습니까?
Nirmit Dagly 2015 년

이 스레드의 모든 링크를 업데이트하고 있습니다. 아래 내 대답에있는 하나를 사용하십시오 ( github.com/SebastienThiebaud/dispatch_cancelable_block )
maggix

Google지도를 사용하는 경우에도 이것을보세요. 이것은 iOS 8과 호환되며 objective-c로 작성되었습니다. github.com/hkellaway/HNKGooglePlacesAutocomplete
Nirmit Dagly 2015-09-16

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 솔루션의 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)
}
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.