问题
After spending some time creating a @Debounced property wrapper I'm not happy with the readability of the code. To understand what's going on you really need to understand how a Property wrapper works and the concept of the wrappedvalue and projectedvalue. This is the Property Wrapper:
@propertyWrapper
class Debounced<Input: Hashable> {
private var delay: Double
private var _value: Input
private var function: ((Input) -> Void)?
private weak var timer: Timer?
public init(wrappedValue: Input, delay: Double) {
self.delay = delay
self._value = wrappedValue
}
public var wrappedValue: Input {
get {
return _value
}
set(newValue) {
timer?.invalidate()
timer = Timer.scheduledTimer(withTimeInterval: delay, repeats: false, block: { [weak self] _ in
self?._value = newValue
self?.timer?.invalidate()
self?.function?(newValue)
})
}
}
public var projectedValue: ((Input) -> Void)? {
get {
return function
}
set(newValue) {
function = newValue
}
}
}
The property wrapper is being used like this:
@Debounced(delay: 0.4) var text: String? = nil
override func viewDidLoad() {
super.viewDidLoad()
self.$text = { text in
print(text)
}
}
It works as it should. Every time the text property is being set, the print function is being called. And if the value is updated more than once within 0.4 seconds then the function will only be called once.
BUT in terms of simplicity and readability, I think its better just creating a Debouncer class like this: https://github.com/webadnan/swift-debouncer.
What do you think? Is there a better way to create this property wrapper?
回答1:
It works as it should ... In that case, just use it!
Hm ... but how to use it? In reality, it is not very flexible, especially till compiler claims "Multiple property wrappers are not supported" :-)
If your goal is to use it in UIKit or SwiftUI app, I suggest you different approach.
Lets try some minimalistic, but fully working SwiftUI example
//
// ContentView.swift
// tmp031
//
// Created by Ivo Vacek on 26/01/2020.
// Copyright © 2020 Ivo Vacek. NO rights reserved.
//
import SwiftUI
import Combine
class S: ObservableObject {
@Published var text: String = ""
@Published var debouncedText: String = ""
private var store = Set<AnyCancellable>()
init(delay: Double) {
$text
.debounce(for: .seconds(delay), scheduler: RunLoop.main)
.sink { [weak self] (s) in
self?.debouncedText = s
}.store(in: &store)
}
}
struct ContentView: View {
@ObservedObject var model = S(delay: 2)
var body: some View {
List {
Color.clear
Section(header: Text("Direct")) {
Text(model.text).font(.title)
}
Section(header: Text("Debounced")) {
Text(model.debouncedText).font(.title)
}
Section(header: Text("Source")) {
TextField("type here", text: $model.text).font(.title)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
You still can subscribe to model.$debouncedText
which is Publisher as many times, as you need. And if you like to use your own action to be performed, no problem as well!
model.$debouncedText
.sink { (s) in
doSomethingWithDebouncedValue(s)
}
Example application usage
UPDATE: if you not able to use Combine, but you like similar syntax ... First define the protokol
protocol Debounce: class {
associatedtype Value: Hashable
var _value: Value { get set }
var _completions: [(Value)->Void] { get set}
var _delay: TimeInterval { get set }
var _dw: DispatchWorkItem! { get set }
func debounce(completion: @escaping (Value)->Void)
}
and default implementation of debounce function. The idea is, to use debounce the same way, as .publisher.sink() on Combine. _debounce is "internal" implementation of debouncing functionality. It compare current and "delay" old value and if they are equal, do the job.
extension Debounce {
func debounce(completion: @escaping (Value)->Void) {
_completions.append(completion)
}
func _debounce(newValue: Value, delay: TimeInterval, completions: [(Value)->Void]) {
if _dw != nil {
_dw.cancel()
}
var dw: DispatchWorkItem!
dw = DispatchWorkItem(block: { [weak self, newValue, completions] in
if let s = self, s._value == newValue {
for completion in completions {
completion(s._value)
}
}
dw = nil
})
_dw = dw
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: dw)
}
}
Now we have all componets of our property wrapper.
@propertyWrapper class Debounced<T: Hashable> {
final class Debouncer: Debounce {
typealias Value = T
var _completions: [(T) -> Void] = []
var _delay: TimeInterval
var _value: T {
willSet {
_debounce(newValue: newValue, delay: _delay, completions: _completions)
}
}
var _dw: DispatchWorkItem!
init(_value: T, _delay: TimeInterval) {
self._value = _value
self._delay = _delay
}
}
var wrappedValue: T {
get { projectedValue._value }
set { projectedValue._value = newValue }
}
var projectedValue: Debouncer
init(wrappedValue: T, delay: TimeInterval) {
projectedValue = Debouncer(_value: wrappedValue, _delay: delay)
}
deinit {
print("deinit")
}
}
lets try it
do {
struct S {
@Debounced(delay: 0.2) var value: Int = 0
}
let s = S()
print(Date(), s.value, "initial")
s.$value.debounce { (i) in
print(Date(), i, "debounced A")
}
s.$value.debounce { (i) in
print(Date(), i, "debounced B")
}
var t = 0.0
(0 ... 8).forEach { (i) in
let dt = Double.random(in: 0.0 ... 0.6)
t += dt
DispatchQueue.main.asyncAfter(deadline: .now() + t) { [t] in
s.value = i
print(s.value, t)
}
}
}
which prints something like
2020-02-04 09:53:11 +0000 0 initial
0 0.46608517831539165
2020-02-04 09:53:12 +0000 0 debounced A
2020-02-04 09:53:12 +0000 0 debounced B
1 0.97078412234771
2 1.1756938500918692
3 1.236562020385944
4 1.4076127046937024
2020-02-04 09:53:13 +0000 4 debounced A
2020-02-04 09:53:13 +0000 4 debounced B
5 1.9313412744029004
6 2.1617775513150366
2020-02-04 09:53:14 +0000 6 debounced A
2020-02-04 09:53:14 +0000 6 debounced B
7 2.6665465865810205
8 2.9287734023206418
deinit
来源:https://stackoverflow.com/questions/59896772/debounced-property-wrapper