Throttling calls in Swift
Throttling wraps a block of code with throttling logic, guaranteeing that an action will never be called more than once each specified interval.
Throttle is typically used inside search boxes in order to limit the number of backend requests while user is typing for a query; without throttling when an user types fast, backend server may receive tons of non useful request which are quite costly.
Moreover client will be busy updating continuously the UI with no longer relevant results: the entire behaviour causes your app to look cheap and the logic unnecessary complex.
Usually when implementing this kind of feature the first options you may consider is to use an
NSTimer fired/invalidated at your set interval. Repeating this boilerplate code in your view controller its not a good idea; usually you want to avoid any mess logic tracking state in a portion of your code that you’ve been trying to keep stateless.
Another solution involves RxSwift which has a throttle implementation out of the box; btw including an entire lib and use just a function its not a good practice after all.
The following implementation uses GCD to execute specified code to throttle wrapped inside a
DispatchWorkItem .
The idea is to specify the maximum interval (in seconds) to execute the job; throttle function must be called at each change of the state (in our search box example you may want call it on
textDidChange ); once called previous queued
DispatchWorkItem is automatically cancelled and a new one is scheduled to get executed after your min interval.
Logic is pretty simple as also the implementation.
Thanks to my friend Ignazio Calò for pointing me to a less verbose version of the class and fixing a leak with the job’s closure.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
// Throttler.swift // // Created by Daniele Margutti on 10/19/2017 // // web: http://www.danielemargutti.com // email: hello@danielemargutti.com // // Updated by Ignazio Calò on 19/10/2017. import UIKit import Foundation public class Throttler { private let queue: DispatchQueue = DispatchQueue.global(qos: .background) private var job: DispatchWorkItem = DispatchWorkItem(block: {}) private var previousRun: Date = Date.distantPast private var maxInterval: Int init(seconds: Int) { self.maxInterval = seconds } func throttle(block: @escaping () -> ()) { job.cancel() job = DispatchWorkItem(){ [weak self] in self?.previousRun = Date() block() } let delay = Date.second(from: previousRun) > maxInterval ? 0 : maxInterval queue.asyncAfter(deadline: .now() + Double(delay), execute: job) } } private extension Date { static func second(from referenceDate: Date) -> Int { return Int(Date().timeIntervalSince(referenceDate).rounded()) } } |
An example with UISearchBar
The following example describe a typical usage of the throttle function with an UISearchBar.
In order to avoid some boilerplate code I’ve also created a subclass of the UISearchBar which implements the UISearchBarDelegate and exposes the most important functions.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
import UIKit public class SearchBar: UISearchBar, UISearchBarDelegate { /// Throttle engine private var throttler: Throttler? = nil /// Throttling interval public var throttlingInterval: Double? = 0 { didSet { guard let interval = throttlingInterval else { self.throttler = nil return } self.throttler = Throttler(seconds: interval) } } /// Event received when cancel is pressed public var onCancel: (() -> (Void))? = nil /// Event received when a change into the search box is occurred public var onSearch: ((String) -> (Void))? = nil public override func awakeFromNib() { super.awakeFromNib() self.delegate = self } // Events for UISearchBarDelegate public func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { self.onCancel?() } public func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { self.onSearch?(self.text ?? "") } public func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { guard let throttler = self.throttler else { self.onSearch?(searchText) return } throttler.throttle { DispatchQueue.main.async { self.onSearch?(self.text ?? "") } } } } |
We have the following events we can subscribe:
- onCancel fired when user tap the cancel button of the search bar.
- onSearch fired when text in search bar’s text field did change; this event is wrapped in our Throttle instance so the only thing we need to configure is the throttlingInterval (if nil throttle will be disabled).
Our view controller may implement it with the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// Set the throttle interval; you will receive a new // value to search for an interval >= 0.5 seconds. searchBar.throttlingInterval = 0.5 // Receive events for search searchBar.onSearch = { text in if text.length == 0 { // user tapped the 'X' button // restore our plain list without search filter } else if textToSearch.length > MIN_CHARS_TO_SEARCH { // A valid search (where length > MIN_CHARS_TO_SEARCH) must be // performed. } } |
With just few line of codes we’ve cleanup our controller and added throttling feature to our UISearchBar .
Questions or suggestions? Follow this tweet:
Throttling calls in Swift #swiftlang #iosdev #throttle https://t.co/SgJ6fk3GiN pic.twitter.com/4WMVykxsFx
— daniele margutti (@danielemargutti) October 19, 2017