This current can be done with a property wrapper:
@propertyWrapper class SetterResponsive<T, U> {
var wrappedValue: (U, T)!
private var method: (U) -> () -> Void
private var runLoopObserver: CFRunLoopObserver!
private var runLoop: CFRunLoop
init(_ method: @escaping (U) -> () -> Void) {
self.method = method
runLoop = CFRunLoopGetCurrent()
runLoopObserver = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, CFRunLoopActivity.beforeSources.rawValue, true, 0) { [unowned self] _, _ in
guard let value = self.wrappedValue else { return }
self.method(value.0)()
}
CFRunLoopAddObserver(runLoop, runLoopObserver, .defaultMode)
}
deinit { CFRunLoopRemoveObserver(runLoop, runLoopObserver, .defaultMode) }
}
It could use the CFRunLoopObserver pattern and listen for a run loop entry, executing the provided method on the next callback execution. For, example:
class Deferred {
@SetterResponsive(_layoutIfNeededComingFromRunLoopSource) private var needsLayoutUpdate: (Deferred, Bool)!
init() { needsLayoutUpdate = (self, false) }
func layoutIfNeeded() { print("Laying out!") }
private func _layoutIfNeededComingFromRunLoopSource() {
guard needsLayoutUpdate.1 else { return }
needsLayoutUpdate.1 = false
layoutIfNeeded()
}
func setNeedsLayout() {
needsLayoutUpdate.1 = true
}
}
would call _layoutIfNeededComingFromRunLoopSource the next RunLoop cycle after setNeedsLayout is called. One major thing to note, the wrapper would need to be defined in Foundation, as RunLoop is only available from Foundation.
I agree this isn't exactly convenient though, and requires a bit of boilerplate (one target-boolean pair per method/method combination)
Personally, I'd like to see an @functionWrapper language feature that allowed us to define custom execution based on a direct method call, cutting out the need to store the boolean & having a private inner method. Something like:
@functionWrapper class RunLoopMethodDispatch<T> {
weak var wrappedTarget: T? // Required by `@functionWrapper`
private var method: (T) -> () -> Void
private var runLoopObserver: CFRunLoopObserver?
private var runLoop: CFRunLoop?
private var currentWhen: When
enum When {
case immediately
case nextRunLoopCycle
}
init(_ method: @escaping (T) -> () -> Void, when: When) {
self.method = method
currentWhen = when
if when == .nextRunLoopCycle {
runLoop = CFRunLoopGetCurrent()
runLoopObserver = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, CFRunLoopActivity.beforeSources.rawValue, true, 0) { [unowned self] _, _ in
guard let target = self.wrappedTarget else { return }
self.method(target)()
}
CFRunLoopAddObserver(runLoop, runLoopObserver, .defaultMode)
}
}
deinit { CFRunLoopRemoveObserver(runLoop, runLoopObserver, .defaultMode) }
func wrappedMethodInvoked() { // Required by `@functionWrapper`
guard currentWhen == .immediately else { return } // We'll dispatch on a run loop cycle, NOT when the method is invoked
if let target = wrappedTarget {
method(target)()
}
}
}
where wrappedMethodInvoked would be called whenever the wrapped method was called:
class Deferred {
...
@RunLoopMethodDispatch(layoutIfNeeded, when: .nextRunLoopCycle)
func setNeedsLayout() {
// Do something in here... maybe pre-setup?
}
}