How to wait for a given time and only perform last

2019-06-20 11:02发布

问题:

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 didSets 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?

回答1:

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.



回答2:

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



回答3:

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.