programing

iOS UI 검색바에서 검색(타이핑 속도 기준)을 조절하는 방법은?

topblog 2023. 9. 20. 20:02
반응형

iOS UI 검색바에서 검색(타이핑 속도 기준)을 조절하는 방법은?

로컬 CoreData와 원격 API 모두에서 검색 결과를 표시하는 데 사용되는 UI SearchDisplayController의 UI SearchBar 부분이 있습니다.제가 이루고 싶은 것은 원격 API의 검색 '지연'입니다.현재 사용자가 입력한 각 문자에 대해 요청이 전송됩니다.그러나 사용자가 특히 빨리 입력하는 경우, 많은 요청을 보내는 것은 말이 되지 않습니다. 그가 입력을 멈출 때까지 기다리는 것이 도움이 될 것입니다.그것을 달성할 수 있는 방법이 있습니까?

설명서를 읽는 것은 사용자가 명시적으로 검색을 탭할 때까지 기다리라고 제안하지만, 저는 그것이 제 경우에 이상적이라고 생각하지 않습니다.

성능 문제.검색 작업을 매우 신속하게 수행할 수 있다면 위임 개체에 searchBar:textDidChange: 메서드를 구현하여 사용자가 입력하는 대로 검색 결과를 업데이트할 수 있습니다.그러나 검색 작업에 시간이 더 걸리는 경우 사용자가 검색 버튼을 누를 때까지 기다렸다가 SearchBarSearchButtonClicked: 메서드에서 검색을 시작해야 합니다.기본 스레드를 차단하지 않도록 항상 백그라운드 스레드를 검색합니다.이렇게 하면 검색이 실행되는 동안 앱이 사용자에게 반응할 수 있고 더 나은 사용자 환경을 제공할 수 있습니다.

API에 많은 요청을 보내는 것은 로컬 성능의 문제가 아니라 원격 서버에서 너무 높은 요청률을 피하기 위한 것일 뿐입니다.

감사해요.

다음 마법을 시도해 보십시오.

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

이 예제에서는 reload라는 메서드를 부르지만 원하는 메서드를 호출할 수 있습니다.

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)
}
@objc func reload() {
    print("Doing things")
}

향상된 Swift 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(Target:)를 구현하는 목적은 에 대한 지속적인 호출을 방지하는 것입니다.reload()"를 마다(c"우"고),reload()가 추가된 문자 수를 기준으로 3번 호출됩니다).

개선점은 다음과 같습니다.reload()메서드에는 검색 막대인 보낸 사람 매개 변수가 있습니다. 따라서 해당 텍스트에 액세스하거나 메서드/속성에 액세스하면 클래스의 글로벌 속성으로 선언할 수 있습니다.

링크 덕분에 저는 매우 빠르고 깨끗한 접근법을 찾았습니다.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]; 
    });
}

주의:

  • loadPlacesAutocompleteForInputLPGoogle Functions 라이브러리의 일부입니다.
  • searchBlockDelay됩니다 됩니다.@implementation:

    정적 CGFloat searchBlockDelay = 0.2;

빠른 해킹은 다음과 같습니다.

- (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초 동안 입력을 멈춘 후에만 업데이트됩니다.

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

자동 검색 타이머는 해제 시 자체 정리를 처리하므로 사용자 코드에서 이에 대해 걱정할 필요가 없습니다.하지만 타이머에 자기에 대한 강력한 참조를 주지 않으면 참조 사이클이 생성됩니다.

아래 구현에서는 타이머를 사용하지만, 원한다면 디스패치 작업 측면에서 재캐스팅할 수 있습니다.

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

}

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

코코아 컨트롤에서 찾은 다음 코드를 참조해 주시기 바랍니다.그들은 데이터를 가져오기 위해 비동기식으로 요청을 보내고 있습니다.로컬에서 데이터를 얻는 것일 수도 있지만 원격 API로 시도해 볼 수 있습니다.백그라운드 스레드의 원격 API에서 비동기 요청을 보냅니다.다음 링크를 따릅니다.

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

사용할 수 있습니다.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를 사용하여 블록 실행 조절에 대한 자세한 내용

Reactive Cocoa를 사용하는 경우,throttle메소드 온RACSignal

여기 스위프트의 스로틀 핸들러가 있습니다. 관심이 있으신 분은

사용가능DispatchWorkItem스위프트 4.0 또는 그 이상을 사용합니다.훨씬 더 쉽고 이해가 됩니다.

사용자가 0.25초 동안 입력하지 않았을 때 API 호출을 실행할 수 있습니다.

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

당신은 여기서 그것에 대한 전체 기사를 읽을 수 있습니다.

  • 공개자:저는 작가입니다.

바닐라 파운데이션 기반 조절 기능이 필요하다면,
reactive, combine, timer, NS Object cancel, 기타 복잡한 것으로 들어가지 않고 하나의 라이너 API만 원한다면,

스로틀러는 작업을 수행하는 데 적합한 도구일 수 있습니다.

다음과 같이 반응하지 않고 조절을 사용할 수 있습니다.

import Throttler

for i in 1...1000 {
    Throttler.go {
        print("throttle! > \(i)")
    }
}

// throttle! > 1000

import UIKit

import Throttler

class ViewController: UIViewController {
    @IBOutlet var button: UIButton!
    
    var index = 0
    
    /********
    Assuming your users will tap the button, and 
    request asyncronous network call 10 times(maybe more?) in a row within very short time nonstop.
    *********/
    
    @IBAction func click(_ sender: Any) {
        print("click1!")
        
        Throttler.go {
        
            // Imaging this is a time-consuming and resource-heavy task that takes an unknown amount of time!
            
            let url = URL(string: "https://jsonplaceholder.typicode.com/todos/1")!
            let task = URLSession.shared.dataTask(with: url) {(data, response, error) in
                guard let data = data else { return }
                self.index += 1
                print("click1 : \(self.index) :  \(String(data: data, encoding: .utf8)!)")
            }
        }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }
}
click1!
click1!
click1!
click1!
click1!
click1!
click1!
click1!
click1!
click1!
2021-02-20 23:16:50.255273-0500 iOSThrottleTest[24776:813744] 
click1 : 1 :  {
  "userId": 1,
  "id": 1,
  "title": "delectus aut autem",
  "completed": false
}

특정 지연 초를 원하는 경우:


import Throttler

for i in 1...1000 {
    Throttler.go(delay:1.5) {
        print("throttle! > \(i)")
    }
}

// throttle! > 1000

스위프트 5.0

에 기반을 둔GSnyder대답

//
//  AutoSearchManager.swift
//  BTGBankingCommons
//
//  Created by Matheus Gois on 01/10/21.
//

import Foundation


/// Manage two timers to implement a standard auto search 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.
public class AutoSearchManager {

    // MARK: - Properties

    private let shortInterval: TimeInterval
    private let longInterval: TimeInterval
    private let callback: (Any?) -> Void

    private var shortTimer: Timer?
    private var longTimer: Timer?

    // MARK: - Lifecycle

    public init(
        short: TimeInterval = Constants.shortAutoSearchDelay,
        long: TimeInterval = Constants.longAutoSearchDelay,
        callback: @escaping (Any?) -> Void
    ) {
        shortInterval = short
        longInterval = long
        self.callback = callback
    }

    // MARK: - Methods

    public func activate(_ object: Any? = nil) {
        shortTimer?.invalidate()
        shortTimer = Timer.scheduledTimer(
            withTimeInterval: shortInterval,
            repeats: false
        ) { [weak self] _ in self?.fire(object) }

        if longTimer == nil {
            longTimer = Timer.scheduledTimer(
                withTimeInterval: longInterval,
                repeats: false
            ) { [weak self] _ in self?.fire(object) }
        }
    }

    public func cancel() {
        shortTimer?.invalidate()
        longTimer?.invalidate()
        shortTimer = nil
        longTimer = nil
    }

    // MARK: - Private methods

    private func fire(_ object: Any? = nil) {
        cancel()
        callback(object)
    }
}

// MARK: - Constants

extension AutoSearchManager {
    public enum Constants {
        /// Auto-search at least this frequently while typing
        public static let longAutoSearchDelay: TimeInterval = 2.0
        /// Trigger automatically after a pause of this length
        public static let shortAutoSearchDelay: TimeInterval = 0.75
    }
}

언급URL : https://stackoverflow.com/questions/24330056/how-to-throttle-search-based-on-typing-speed-in-ios-uisearchbar

반응형