IOS UISearchBar में थ्रॉटल सर्च (टाइपिंग स्पीड के आधार पर) कैसे करें?


80

मेरे पास UISearchDisplayController का एक UISearchBar हिस्सा है जो स्थानीय CoreData और दूरस्थ API दोनों से खोज परिणाम प्रदर्शित करने के लिए उपयोग किया जाता है। जो मैं हासिल करना चाहता हूं वह दूरस्थ एपीआई पर खोज की "देरी" है। वर्तमान में, उपयोगकर्ता द्वारा टाइप किए गए प्रत्येक चरित्र के लिए, एक अनुरोध भेजा जाता है। लेकिन अगर उपयोगकर्ता विशेष रूप से तेजी से टाइप करता है, तो कई अनुरोध भेजने का कोई मतलब नहीं है: यह तब तक इंतजार करने में मदद करेगा जब तक वह टाइप करना बंद नहीं कर देता। वहाँ एक तरीका है कि प्राप्त करने के लिए है?

प्रलेखन को पढ़ने से यह पता चलता है कि जब तक उपयोगकर्ता खोज पर स्पष्ट रूप से टैप नहीं करते हैं, तब तक प्रतीक्षा करें, लेकिन मुझे अपने मामले में यह आदर्श नहीं लगता।

प्रदर्शन के कारण। यदि खोज संचालन बहुत तेज़ी से किया जा सकता है, तो खोज परिणामों को अपडेट करना संभव है क्योंकि उपयोगकर्ता खोज में लागू करके टाइप कर रहा है: textDidChange: प्रतिनिधि ऑब्जेक्ट पर विधि। हालाँकि, यदि खोज अभियान में अधिक समय लगता है, तो आपको तब तक इंतजार करना चाहिए जब तक उपयोगकर्ता खोज में टैप नहीं करता है खोज में खोज शुरू करने से पहले खोज बटनबटनटन: विधि। मुख्य थ्रेड को ब्लॉक करने से बचने के लिए हमेशा सर्च ऑपरेशन एक बैकग्राउंड थ्रेड करें। यह आपके ऐप को उपयोगकर्ता के लिए उत्तरदायी रखता है जबकि खोज चल रही है और एक बेहतर उपयोगकर्ता अनुभव प्रदान करता है।

एपीआई में कई अनुरोध भेजना स्थानीय प्रदर्शन की समस्या नहीं है, लेकिन केवल दूरस्थ सर्वर पर बहुत अधिक अनुरोध दर से बचने के लिए है।

धन्यवाद


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

स्विफ्ट संस्करण:

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

इस उदाहरण पर ध्यान दें एक विधि जिसे पुनः लोड कहा जाता है, लेकिन आप इसे कॉल कर सकते हैं जो भी विधि आपको पसंद हो!


यह महान काम करता है ... रद्द करने के बारे में पता नहीं है।
jesses.co.tt

आपका स्वागत है! यह एक बेहतरीन पैटर्न है और इसे हर तरह की चीजों के लिए इस्तेमाल किया जा सकता है।
मल्हल

इतना उपयोगी! यह असली वूडू है
मट्टियो पैसिनी

2
"रीलोड" के बारे में ... मुझे इसके बारे में एक दो सेकंड के लिए सोचना था ... यह स्थानीय पद्धति को संदर्भित करता है जो उपयोगकर्ता द्वारा 0.5 सेकंड के लिए टाइपिंग बंद करने के बाद वास्तव में वह सामान करना होगा जो आप करना चाहते हैं। विधि को आप जो चाहें, सर्चएक्सच्यूट कह सकते हैं। धन्यवाद!
मूलांक

यह मेरे लिए काम नहीं करता है ... यह हर बार एक पत्र बदलने के बाद "पुनः लोड" फ़ंक्शन को जारी रखता है
एंड्री

52

जिन लोगों को स्विफ्ट 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)
}

संपादित करें: स्विफ्ट 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
अच्छा उत्तर! मैंने इसमें थोड़ा सुधार किया है, आप इसे देख सकते हैं :)
अहमद एफ

धन्यवाद @ अहमदाबाद, मैं एक स्विफ्ट 4 अपडेट करने के बारे में सोच रहा था। तुमने यह किया! : डी
विवियन जी

1
स्विफ्ट 4 के लिए, DispatchWorkItemपहले सुझाए गए अनुसार उपयोग करें । यह चयनकर्ताओं की तुलना में सुरुचिपूर्ण ढंग से काम करता है।
टेफी

21

बेहतर स्विफ्ट 4:

यह मानते हुए कि आप पहले से ही इसके अनुरूप हैं UISearchBarDelegate, यह VivienG के उत्तर का एक बेहतर स्विफ्ट 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)
}

रद्द करने का उद्देश्य PrepretPerformRequests (withTarget :)reload() खोज बार में प्रत्येक परिवर्तन के लिए निरंतर कॉलिंग को रोकने के लिए है (इसे जोड़े बिना, यदि आपने "abc" टाइप किया है, तो reload()जोड़े गए वर्णों की संख्या के आधार पर तीन बार कहा जाएगा) ।

सुधार है: में reload()विधि इस पैरामीटर जो खोज पट्टी है; इस प्रकार इसकी किसी भी विधि / संपत्तियों को अपने पाठ तक पहुँचाना- इसे कक्षा में एक वैश्विक संपत्ति घोषित करने के साथ सुलभ होगा।


मेरे लिए यह वास्तव में उपयोगी है, चयनकर्ता में खोज बार की वस्तु के साथ पार्सिंग
हरि नारायणन

मैंने अभी OBJC में कोशिश की - (शून्य) searchBar: (UISearchBar *) searchBar textDidChange: (NSString *) searchText {[NSObject CancelPrepretPerpretRequestsWithTarget: self selector: @selector (validateText :) ऑब्जेक्ट: searchBar; [आत्म-प्रदर्शनकर्ता: @selector (validateText :) withObject: searchBar afterDelay: 0.5]; }
हरि नारायणन

18

इस लिंक के लिए धन्यवाद , मुझे एक बहुत ही त्वरित और स्वच्छ दृष्टिकोण मिला। निरमित के उत्तर की तुलना में इसमें "लोडिंग इंडिकेटर" का अभाव है, हालांकि यह कोड की संख्या के संदर्भ में जीतता है और अतिरिक्त नियंत्रण नहीं करता है। मैंने पहले 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]; 
    });
}

टिप्पणियाँ:

  • loadPlacesAutocompleteForInputका हिस्सा है LPGoogleFunctions पुस्तकालय
  • searchBlockDelayनिम्न के रूप में परिभाषित किया गया है @implementation:

    स्थिर CGFloat searchBlockDelay = 0.2;


1
ब्लॉग पोस्ट का लिंक मेरे लिए मृत प्रतीत होता है
21

1
@ जेरेन आप सही हैं: दुर्भाग्य से ऐसा लगता है कि लेखक ने अपनी वेबसाइट से ब्लॉग को हटा दिया। GitHub पर उस ब्लॉग को संदर्भित करने का भंडार अभी भी बना हुआ है, इसलिए आप यहाँ कोड की जाँच करना चाह सकते हैं: github.com/SebastienThiebaud/dispatch_cancelable_block
maggaya

खोजब्लॉक के अंदर का कोड कभी भी निष्पादित नहीं होता है। क्या अधिक कोड आवश्यक है?
प्रातः

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
मैगिक्स

हां, अपनी आवश्यकताओं के अनुसार इसे संशोधित करें। अवधारणा एक ही है।
duci9y

3
चूंकि इस दृष्टिकोण में टाइमर को कभी भी निकाल नहीं दिया जाता है, इसलिए मुझे पता चला कि यहां एक लाइन गायब है: [[NSRunLoop mainRunLoop] addTimer: टाइमर forMode: NSDefaultRunLoopMode];
प्रातः

@itinance क्या मतलब है आपका? जब आप कोड में विधि के साथ बनाते हैं तो टाइमर पहले से ही चालू रन लूप में होता है।
ड्यूसी

यह एक त्वरित और साफ समाधान है। आप अपने अन्य नेटवर्क अनुरोधों में भी इसका उपयोग कर सकते हैं, जैसे कि मेरी स्थिति में, मैं हर बार नया डेटा प्राप्त करता हूं, जब उपयोगकर्ता अपना नक्शा खो देता है। बस ध्यान दें कि स्विफ्ट में, आप कॉल करके अपने टाइमर ऑब्जेक्ट को इंस्टेंट करना चाहेंगे scheduledTimer...
ग्लेन पोसादास

5

स्विफ्ट 4 समाधान, साथ ही कुछ सामान्य टिप्पणियां:

ये सभी उचित दृष्टिकोण हैं, लेकिन यदि आप अनुकरणीय ऑटोसर्च व्यवहार चाहते हैं, तो आपको वास्तव में दो अलग-अलग टाइमर या प्रेषण चाहिए।

आदर्श व्यवहार यह है कि 1) ऑटोसर्च को समय-समय पर ट्रिगर किया जाता है, लेकिन 2) बहुत बार नहीं (सर्वर लोड, सेलुलर बैंडविड्थ और यूआई स्टुटर्स पैदा करने की क्षमता के कारण), और 3) जैसे ही एक ठहराव होता है, यह तेजी से चालू हो जाता है। उपयोगकर्ता की टाइपिंग।

आप एक लंबी अवधि के टाइमर के साथ इस व्यवहार को प्राप्त कर सकते हैं जो संपादन शुरू होते ही चालू हो जाता है (मुझे 2 सेकंड का सुझाव है) और बाद की गतिविधि की परवाह किए बिना चलाने की अनुमति है, साथ ही एक अल्पकालिक टाइमर (~ 0.75 सेकंड) जो हर पर रीसेट है परिवर्तन। या तो टाइमर की समाप्ति स्वतः खोज को ट्रिगर करती है और दोनों टाइमर को रीसेट करती है।

शुद्ध प्रभाव यह है कि निरंतर टाइपिंग से हर लंबी अवधि के सेकंड में ऑटोसर्च की पैदावार होती है, लेकिन छोटी अवधि के सेकंड में ऑटोसर्च को ट्रिगर करने के लिए एक ठहराव की गारंटी दी जाती है।

आप नीचे दिए गए ऑटोसर्चटाइमर वर्ग के साथ इस व्यवहार को बहुत सरलता से लागू कर सकते हैं। इसका उपयोग कैसे करें:

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

स्वतन्त्र खोजकर्ता मुक्त होने पर अपना स्वयं का क्लीनअप संभालता है, इसलिए अपने कोड में इसके बारे में चिंता करने की कोई आवश्यकता नहीं है। लेकिन टाइमर को स्वयं के लिए एक मजबूत संदर्भ न दें या आप एक संदर्भ चक्र बनाएंगे।

नीचे दिए गए कार्यान्वयन में टाइमर का उपयोग किया गया है, लेकिन यदि आप चाहें तो आप इसे प्रेषण कार्यों के संदर्भ में फिर से तैयार कर सकते हैं।

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

कृपया निम्न कोड देखें जो मुझे कोको नियंत्रण पर मिला है। वे डेटा लाने के लिए एसिंक्रोनस रूप से अनुरोध भेज रहे हैं। हो सकता है कि वे स्थानीय से डेटा प्राप्त कर रहे हों, लेकिन आप इसे दूरस्थ एपीआई के साथ आज़मा सकते हैं। बैकग्राउंड थ्रेड में दूरस्थ एपीआई पर async अनुरोध भेजें। नीचे दिए गए लिंक का पालन करें:

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


नमस्ते! मेरे पास आपके सुझाए गए नियंत्रण पर एक नज़र रखने का समय है। यह निश्चित रूप से दिलचस्प है और मुझे इसमें कोई संदेह नहीं है कि कई लोग इससे लाभान्वित होंगे। हालाँकि मुझे लगता है कि मैंने इस ब्लॉग पोस्ट से एक छोटा (और, मेरी राय में, क्लीनर) समाधान पाया, आपके लिंक से कुछ प्रेरणा के लिए धन्यवाद: sebastienthiebaud.us/blog/ios/gcd/block/2014/04/04/…
मैगिक्स

@ मैगिक्स ने आपके द्वारा दिया गया लिंक अब समाप्त हो गया है। क्या आप किसी अन्य लिंक का सुझाव दे सकते हैं
निरमित डगली

मैं इस थ्रेड के सभी लिंक अपडेट कर रहा हूं। नीचे (मेरा उत्तर में एक का प्रयोग करें github.com/SebastienThiebaud/dispatch_cancelable_block )
maggix

इसे भी देखें, यदि आप Google मानचित्र का उपयोग कर रहे हैं। यह iOS 8 के साथ संगत है और ऑब्जेक्टिव-सी में लिखा गया है। github.com/hkellaway/HNKGooglePlacesAutocomplete
Nirmit Dagly

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

थ्रॉटलिंग पर अधिक जीसीडी का उपयोग करके एक ब्लॉक निष्पादन

यदि आप ReactiveCocoa का उपयोग कर रहे हैं , तो throttleविधि पर विचार करेंRACSignal

यहाँ आप रुचि रखते हैं स्विफ्ट में थ्रॉटलहैंडलर है


मुझे लगता है कि github.com/SebastienThiebaud/dispatch_cancelable_block/blob/… उपयोगी होने के लिए, भी
onmyway133

3

NSTimer समाधान का 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)
}

-1

आप स्विफ्ट 4.0 या इसके बाद के संस्करण के DispatchWorkItemसाथ उपयोग कर सकते हैं । यह बहुत आसान है और समझ में आता है।

जब उपयोगकर्ता 0.25 सेकंड के लिए टाइप नहीं करता है तो हम एपीआई कॉल को निष्पादित कर सकते हैं।

class SearchViewController: UIViewController, UISearchBarDelegate {
// We keep track of the pending work item as a property
private var pendingRequestWorkItem: DispatchWorkItem?

func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
    // Cancel the currently pending item
    pendingRequestWorkItem?.cancel()

    // Wrap our request in a work item
    let requestWorkItem = DispatchWorkItem { [weak self] in
        self?.resultsLoader.loadResults(forQuery: searchText)
    }

    // Save the new work item and execute it after 250 ms
    pendingRequestWorkItem = requestWorkItem
    DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(250),
                                  execute: requestWorkItem)
}
}

आप इसके बारे में पूरा लेख यहाँ से पढ़ सकते हैं

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.