Debounced Property Wrapper

人走茶凉 提交于 2020-06-27 17:26:08

问题


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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!