Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Create generic delegate for class

Let's say I have very simple class:

class Box<T> {
  var boxedObject:T

  init(object: T) {
    self.boxedObject = object
  }
}

What I would like to achieve now is to add delegate, which can inform me that value in box has changed:

protocol BoxDelegate<T>: class {
    func valueInBoxChanged(box: Box<T>) -> Void
}

class Box<T> {
    var boxedObject: T {
        didSet {
            self.delegate?.valueInBoxChanged(self)
        }
    }
    weak var delegate: BoxDelegate<T>?

    init(object: T) {
        self.boxedObject = object
    }
}

This code is of course not working, because we don't have generic delegates. I can make delegate to be a struct with closure, but it's a bit ugly solution. How I should do such things in Swift?

like image 998
Wieczorny Avatar asked Jun 29 '26 12:06

Wieczorny


1 Answers

Let say that instead of BoxDelegate you have a class, and let's call it Listener :

public typealias ValueChanged <T> = (T) -> Void
public class Listener<T> {
    var valueChanged: ValueChanged<T>

    public init(_ valueChanged: @escaping ValueChanged<T>) {
        self.valueChanged = valueChanged
    }
}

Box can now be written like that:

class Box<T> {
    // the listener needs to be weak in order to be deallocated when the that is
    // observing the changes is deallocated
    weak private(set) var listener: Listener<T>?
    init(_ listener: Listener<T>) {
        self.listener = listener
    }
}

and you can now have a wrapper class for the properties whose state you need to observe:

class Observed<T> {
    var listeners: [Box<T>] = []
    private let queue: DispatchQueue = .main
    var value: T {
        didSet {
            notifyListeners()
        }
    }
    func notifyListeners() {
        notifyListeners(value)
    }
    
    func notifyListeners(_ value: T) {
        queue.async { [weak self] in
            self?.listeners.forEach { (box) in
                box.listener?.valueChanged(value)
            }
        }
    }
    func bind(_ listener: Listener<T>) {
        listeners.append(Box(listener))
    }
    
    func bind(_ listener: @escaping ValueChanged<T>) -> Listener<T> {
        let listener = Listener(listener)
        listeners.append(Box(listener))
        return listener
    }
    init(_ value: T) {
        self.value = value
    }
}

and even have a property wrapper:

@propertyWrapper
struct Bind<T> {
    let observable: Observed<T>
    init(wrappedValue: T) {
        observable = Observed(wrappedValue)
    }
    
    var wrappedValue: T {
        get {
            observable.value
        }
        set {
            observable.value = newValue
        }
    }
    
    var projectedValue: Observed<T> {
        observable
    }
}

usage :

var text = ""
class ViewModel {
    @Bind
    var isOn: Bool = false
}

let viewModel = ViewModel()

let bond = viewModel.$isOn.bind { isOn in
    if isOn {
        text = "on"
    } else {
        text = "off"
    }
    
}
viewModel.isOn = true
RunLoop.main.run(until: Date().advanced(by: 0.1))
text
viewModel.isOn = false
RunLoop.main.run(until: Date().advanced(by: 0.1))
text

In a playground

like image 59
AnderCover Avatar answered Jul 01 '26 02:07

AnderCover



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!