Let's say I have a class named Person
, with variables like firstName
and lastName
. I am listening to changes in these variables using a reactiveCocoa-framework, but let's say I'm only using the built in KVO-listening, like the didSet{}
. So assume I have this code:
let firstName:String { didSet{ self.nameDidChange() }}
let lastName: String { didSet{ self.nameDidChange() }}
func nameDidChange(){ print("New name:", firstName, lastName}
Every time I'd change either the first name or the last name, it would automatically call the function nameDidChange
.
What I'm wondering, is if there's any smart move here to prevent the nameDidChange
function from being called twice in a row when I change both the firstName
and the lastName
.
Let's say the value in firstName
is "Anders"
and lastName
is "Andersson"
, then I run this code:
firstName = "Borat"
lastName = "Boratsson"
nameDidChange
will be called twice here. It will first print out "New name: Borat Andersson"
, then "New name: Borat Boratsson"
.
In my simple mind, I'm thinking I can create a function called something like nameIsChanging()
, call it whenever any of the didSet
is called, and start a timer for like 0.1 second, and then call nameDidChange()
, but both of these didSet
s will also call nameIsChanging
, so the timer will go twice, and fire both times. To solve this, I could keep a "global" Timer
, and make it invalidate itself and restart the count or something like that, but the more I think of solutions, the uglier they get. Are there any "best practices" here?
I think you are on the right track. I think you just need to delay the call to name changed until the user has "Stopped" typing.
Something like this:
var timer = Timer()
var firstName: String = "Clint" {
didSet {
timer.invalidate()
timer = Timer.scheduledTimer(withTimeInterval: 0.2, repeats: false, block: { _ in
self.nameDidChange()
})
}
}
var secondName: String = "Eastwood" {
didSet {
timer.invalidate()
timer = Timer.scheduledTimer(withTimeInterval: 0.2, repeats: false, block: { _ in
self.nameDidChange()
})
}
}
func nameDidChange() {
print(firstName + secondName)
}
Everytime the first or second name is changed it will stop the timer and wait another 0.2 seconds until it commits the name change.
Edit
After reading the comment by Adam Venturella I realized that it is indeed a debouncing technique. It would be useful to google the concept if you would like to learn some more about it.
Here is a simple Playground that illustrates the concept:
import UIKit
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
var timer: Timer? = nil
func nameDidChange() {
print("changed")
}
func debounce(seconds: TimeInterval, function: @escaping () -> Swift.Void ) {
timer?.invalidate()
timer = Timer.scheduledTimer(withTimeInterval: seconds, repeats: false, block: { _ in
function()
})
}
debounce(seconds: 0.2) { nameDidChange() }
debounce(seconds: 0.2) { nameDidChange() }
debounce(seconds: 0.2) { nameDidChange() }
debounce(seconds: 0.2) { nameDidChange() }
debounce(seconds: 0.2) { nameDidChange() }
debounce(seconds: 0.2) { nameDidChange() }
The output:
changed
The nameDidChange function was executed only once.
Not sure if I understood your question correctly but instead of timers you can try to utilize Date
to see when if they were fired right after each other or not. Also note that .timeIntervalSince1970
returns the number of seconds since 1970 so I multiplied it by 100 to get a better precision.
var firstName:String! { didSet{ self.nameDidChange() }}
var lastName: String! { didSet{ self.nameDidChange() }}
var currentDate: UInt64 = UInt64((Date().timeIntervalSince1970 * 100)) - 100
func nameDidChange(){
let now = UInt64(Date().timeIntervalSince1970 * 100)
//You can add a higher tolerance here if you wish
if (now == currentDate) {
print("firing in succession")
} else {
print("there was a delay between the two calls")
}
currentDate = now
}
EDIT: Although this doesn't run on the last call but rather the first call but maybe this might help / spark some ideas
An easy way to coalesce calls to nameDidChange()
is to call it via DispatchQueue.main.async
, unless such a call is already pending.
class MyObject {
var firstName: String = "" { didSet { self.scheduleNameDidChange() } }
var lastName: String = "" { didSet { self.scheduleNameDidChange() } }
private func scheduleNameDidChange() {
if nameDidChangeIsPending { return }
nameDidChangeIsPending = true
RunLoop.main.perform { self.nameDidChange() }
}
private func nameDidChange() {
nameDidChangeIsPending = false
print("New name:", firstName, lastName)
}
private var nameDidChangeIsPending = false
}
If a single UI event (e.g. a touch) result in multiple changes to firstName
and lastName
, nameDidChange()
will only be called once.