Làm thế nào để điều chỉnh tìm kiếm (dựa trên tốc độ nhập) trong iOS UISearchBar?


80

Tôi có một phần UISearchBar của UISearchDisplayController được sử dụng để hiển thị kết quả tìm kiếm từ cả CoreData cục bộ và API từ xa. Những gì tôi muốn đạt được là "trì hoãn" tìm kiếm trên API từ xa. Hiện tại, đối với mỗi ký tự do người dùng nhập, một yêu cầu được gửi. Nhưng nếu người dùng gõ đặc biệt nhanh, việc gửi nhiều yêu cầu sẽ không hợp lý: sẽ hữu ích khi đợi cho đến khi anh ta ngừng nhập. Có cách nào để đạt được điều đó không?

Đọc tài liệu cho thấy hãy đợi cho đến khi người dùng nhấn vào tìm kiếm một cách rõ ràng, nhưng tôi không thấy lý tưởng trong trường hợp của mình.

Vấn đề hiệu năng. Nếu các thao tác tìm kiếm có thể được thực hiện rất nhanh, thì có thể cập nhật kết quả tìm kiếm khi người dùng đang nhập bằng cách triển khai phương thức searchBar: textDidChange: trên đối tượng ủy nhiệm. Tuy nhiên, nếu thao tác tìm kiếm mất nhiều thời gian hơn, bạn nên đợi cho đến khi người dùng nhấn vào nút Tìm kiếm trước khi bắt đầu tìm kiếm trong phương thức searchBarSearchButtonClicked :. Luôn thực hiện các thao tác tìm kiếm một chuỗi nền để tránh chặn chuỗi chính. Điều này giúp ứng dụng của bạn phản hồi người dùng trong khi tìm kiếm đang chạy và cung cấp trải nghiệm người dùng tốt hơn.

Việc gửi nhiều yêu cầu tới API không phải là vấn đề về hiệu suất cục bộ mà chỉ để tránh tỷ lệ yêu cầu quá cao trên máy chủ từ xa.

Cảm ơn


1
Tôi không chắc tiêu đề là chính xác. Những gì bạn đang yêu cầu được gọi là "debounce" chứ không phải "ga".
V_tredue

Câu trả lời:


132

Hãy thử phép thuật này:

- (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];
}

Phiên bản 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)
 }

Lưu ý rằng ví dụ này gọi một phương thức được gọi là tải lại nhưng bạn có thể gọi nó bất kỳ phương thức nào bạn thích!


điều này hoạt động tuyệt vời ... không biết về phương thức hủyPreviousPerformRequestsWithTarget!
jesses.co.tt

không có gì! Đó là một mẫu tuyệt vời và có thể được sử dụng cho tất cả mọi thứ.
malhal

Rất hữu ích! Đây là voodoo thực
Matteo Pacini

2
Về "tải lại" ... Tôi đã phải suy nghĩ về nó thêm vài giây ... Điều đó đề cập đến phương thức cục bộ sẽ thực sự thực hiện những thứ bạn muốn làm sau khi người dùng ngừng nhập trong 0,5 giây. Phương thức có thể được gọi là bất cứ thứ gì bạn muốn, như searchExecute. Cảm ơn!
blalond

điều này không làm việc cho tôi ... nó tiếp tục chạy "tải lại" chức năng mỗi khi thư được thay đổi
Andrey

52

Đối với những người cần điều này trong Swift 4 trở đi :

Giữ nó đơn giản với một DispatchWorkItemlike ở đây .


hoặc sử dụng cách cũ của 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)
}

CHỈNH SỬA: Phiên bản 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
Câu trả lời tốt! Tôi chỉ thêm một chút cải tiến cho nó, bạn có thể kiểm tra nó :)
Ahmad F

Cảm ơn @AhmadF, tôi đã nghĩ đến việc cập nhật SWIFT 4. Bạn làm được rồi! : D
VivienG 23/1217

1
Đối với Swift 4, hãy sử dụng DispatchWorkItemnhư đề xuất đầu tiên ở trên. Nó hoạt động thanh lịch hơn bộ chọn.
Teffi

21

Cải tiến Swift 4:

Giả sử rằng bạn đã tuân thủ UISearchBarDelegate, đây là phiên bản Swift 4 được cải tiến trong câu trả lời của 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)
}

Mục đích của việc triển khaicelPreviousPerformRequests (withTarget :) là để ngăn việc gọi liên tục reload()cho mỗi thay đổi đối với thanh tìm kiếm (mà không cần thêm nó, nếu bạn nhập "abc", reload()sẽ được gọi ba lần dựa trên số ký tự được thêm vào) .

Các cải tiến là: trong reload()phương pháp có các tham số người gửi là thanh tìm kiếm; Do đó, việc truy cập văn bản của nó - hoặc bất kỳ phương thức / thuộc tính nào của nó - đều có thể truy cập được bằng cách khai báo nó như một thuộc tính toàn cục trong lớp.


Nó thực sự hữu ích đối với tôi ,, phân tích với mục đích thanh tìm kiếm trong selector
Hari Narayanan

Tôi vừa thử trong OBJC - (void) searchBar: (UISearchBar *) searchBar textDidChange: (NSString *) searchText {[NSObject hủyPreviousPerformRequestsWithTarget: self selector: @selector (validateText :) object: searchBar]; [self performanceSelector: @selector (validateText :) withObject: searchBar afterDelay: 0.5]; }
Hari Narayanan

18

Nhờ liên kết này , tôi đã tìm thấy một cách tiếp cận rất nhanh chóng và sạch sẽ. So với câu trả lời của Nirmit, nó thiếu "chỉ báo tải", tuy nhiên nó thắng về số lượng dòng mã và không yêu cầu điều khiển bổ sung. Đầu tiên tôi được thêm vào dispatch_cancelable_block.htập tin để dự án của tôi (từ repo này ), sau đó định nghĩa các biến lớp sau: __block dispatch_cancelable_block_t searchBlock;.

Mã tìm kiếm của tôi bây giờ trông như thế này:

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

Ghi chú:

  • Đây loadPlacesAutocompleteForInputlà một phần của thư viện LPGoogleFunctions
  • searchBlockDelayđược định nghĩa như sau bên ngoài @implementation:

    tĩnh CGFloat searchBlockDelay = 0,2;


1
Liên kết đến bài đăng trên blog dường như đã chết đối với tôi
jeroen

1
@jeroen bạn nói đúng: tiếc là có vẻ như tác giả đã xóa blog khỏi trang web của mình. Kho trên GitHub mà gọi blog mà vẫn tăng, vì vậy bạn có thể muốn kiểm tra mã ở đây: github.com/SebastienThiebaud/dispatch_cancelable_block
maggix

mã bên trong searchBlock không bao giờ thực thi. Có cần thêm mã không?
khởi hành

12

Một bản hack nhanh sẽ như vậy:

- (void)textViewDidChange:(UITextView *)textView
{
    static NSTimer *timer;
    [timer invalidate];
    timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(requestNewDataFromServer) userInfo:nil repeats:NO];
}

Mỗi khi chế độ xem văn bản thay đổi, bộ hẹn giờ bị vô hiệu, khiến nó không hoạt động. Bộ hẹn giờ mới được tạo và đặt để kích hoạt sau 1 giây. Tìm kiếm chỉ được cập nhật sau khi người dùng ngừng nhập trong 1 giây.


Có vẻ như chúng tôi đã có cùng một cách tiếp cận và cách này thậm chí không yêu cầu mã bổ sung. Mặc dù requestNewDataFromServerphương pháp cần được sửa đổi để có được các thông số từuserInfo
maggix

Đúng, sửa đổi nó theo nhu cầu của bạn. Khái niệm là như nhau.
duci9y

3
vì bộ hẹn giờ không bao giờ được kích hoạt trong cách tiếp cận này, tôi đã phát hiện ra rằng một dòng bị thiếu ở đây: [[NSRunLoop mainRunLoop] addTimer: timer forMode: NSDefaultRunLoopMode];
itinance

@itinance Ý bạn là gì? Bộ đếm thời gian đã ở trong vòng chạy hiện tại khi bạn tạo nó bằng phương thức trong mã.
duci9y

Đây là một giải pháp nhanh chóng và gọn gàng. Bạn cũng có thể sử dụng điều này trong các yêu cầu mạng khác của mình, như trong tình huống của tôi, tôi tìm nạp dữ liệu mới mỗi khi người dùng kéo bản đồ của họ. Chỉ cần lưu ý rằng trong Swift, bạn sẽ muốn khởi tạo đối tượng hẹn giờ của mình bằng cách gọi scheduledTimer....
Glenn Posadas

5

Giải pháp Swift 4, cùng với một số nhận xét chung:

Đây là tất cả các cách tiếp cận hợp lý, nhưng nếu bạn muốn hành vi tự động tìm kiếm mẫu mực, bạn thực sự cần hai bộ hẹn giờ hoặc công văn riêng biệt.

Hành vi lý tưởng là 1) tự động tìm kiếm được kích hoạt định kỳ, nhưng 2) không quá thường xuyên (do tải máy chủ, băng thông di động và khả năng gây ra lỗi giao diện người dùng) và 3) tính năng này sẽ kích hoạt nhanh chóng ngay khi có tạm dừng của người dùng.

Bạn có thể đạt được hành vi này với một bộ hẹn giờ dài hạn hơn được kích hoạt ngay khi bắt đầu chỉnh sửa (tôi đề xuất là 2 giây) và được phép chạy bất kể hoạt động sau đó, cộng với một bộ hẹn giờ ngắn hạn (~ 0,75 giây) được đặt lại vào mỗi thay đổi. Việc hết hạn của một trong hai bộ hẹn giờ sẽ kích hoạt tự động tìm kiếm và đặt lại cả hai bộ hẹn giờ.

Hiệu quả thực sự là việc nhập liên tục mang lại tự động tìm kiếm sau mỗi giây dài, nhưng thời gian tạm dừng được đảm bảo sẽ kích hoạt tự động tìm kiếm trong khoảng thời gian ngắn.

Bạn có thể thực hiện hành vi này rất đơn giản với lớp AutosearchTimer bên dưới. Đây là cách sử dụng nó:

// 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 xử lý quá trình dọn dẹp của chính nó khi được giải phóng, vì vậy không cần phải lo lắng về điều đó trong mã của riêng bạn. Nhưng đừng cung cấp cho bộ đếm thời gian một tham chiếu mạnh về chính nó, nếu không bạn sẽ tạo ra một chu kỳ tham chiếu.

Việc triển khai bên dưới sử dụng bộ hẹn giờ, nhưng bạn có thể chỉnh sửa lại nó trong điều kiện hoạt động điều phối nếu muốn.

// 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

Vui lòng xem mã sau đây mà tôi đã tìm thấy trên các điều khiển ca cao. Họ đang gửi yêu cầu không đồng bộ để tìm nạp dữ liệu. Có thể họ đang lấy dữ liệu từ cục bộ nhưng bạn có thể thử bằng API từ xa. Gửi yêu cầu không đồng bộ trên API từ xa trong chuỗi nền. Theo liên kết dưới đây:

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


Chào! Cuối cùng tôi đã có thời gian để xem xét điều khiển được đề xuất của bạn. Nó chắc chắn là thú vị và tôi không nghi ngờ rằng nhiều người sẽ được hưởng lợi từ nó. Tuy nhiên, tôi nghĩ rằng tôi đã tìm thấy một giải pháp ngắn hơn (và theo ý kiến ​​của tôi, rõ ràng hơn) từ bài đăng blog này, nhờ một số cảm hứng từ liên kết của bạn: sebastienthiebaud.us/blog/ios/gcd/block/2014/04/09/…
maggix

@maggix liên kết bạn cung cấp hiện đã hết hạn. Bạn có thể đề xuất bất kỳ liên kết nào khác.
Nirmit Dagly

Tôi đang cập nhật tất cả các liên kết trong chủ đề này. Sử dụng một trong câu trả lời của tôi dưới đây ( github.com/SebastienThiebaud/dispatch_cancelable_block )
maggix

Cũng nhìn vào điều này, nếu bạn đang sử dụng Google Maps. Điều này tương thích với iOS 8 và được viết trong mục tiêu-c. github.com/hkellaway/HNKGooglePboardsAutocomplete
Nirmit Dagly,

3

Chúng ta có thể sử dụng 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;
}

Thông tin thêm về Điều chỉnh việc thực thi khối bằng GCD

Nếu bạn đang sử dụng ReactiveCocoa , hãy xem xét throttlephương pháp trênRACSignal

Đây là ThrottleHandler trong Swift mà bạn quan tâm



3

Phiên bản Swift 2.0 của giải pháp 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)
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.