Hello! I'm in the process of learning Combine and wanted to try my hand at creating a Publisher
for UIControl.Event
s. I've come up with something, but I'm not very happy with it and I'm looking to get some advice on a hopefully more idiomatic approach.
extension Publishers {
private static var key: UInt8 = 0
public struct ControlForEvent<Control> : Publisher where Control: UIControl {
public typealias Output = Control
public typealias Failure = Never
public let control: Output
public let event: UIControl.Event
public func receive<S>(subscriber: S)
where S : Subscriber,
Output == S.Input,
Failure == S.Failure
{
let subject = PassthroughSubject<Output, Never>()
let passthrough = ControlEventPassthroughSubject(
subject: subject,
control: control,
event: event
)
objc_setAssociatedObject(
subject,
&Publishers.key,
passthrough,
.OBJC_ASSOCIATION_RETAIN
)
subject.receive(subscriber: subscriber)
}
private class ControlEventPassthroughSubject {
unowned var subject: PassthroughSubject<Output, Failure>
init(subject: PassthroughSubject<Output, Never>, control: Output, event: UIControl.Event) {
self.subject = subject
control.addTarget(self, action: #selector(receive(_:)), for: event)
}
@objc
func receive(_ sender: UIControl) {
subject.send(sender as! Output)
}
}
}
}
I've started off by creating my ControlForEvent
publisher inside the Publishers
namespace. I've made it a struct as seems to be the approach a number of similar publishers take (like the DataTaskPublisher
or the ValueForKey
publisher).
I've then created a ControlEventPassthroughSubject
class to listen for the given action, and then forwards all the events to a PassthroughSubject
it receives.
So far so good (maybe), but things feel like they go down hill fast in the receive(subscriber:)
function, specifically the objc_setAssociatedObject
bit. The subscriber appears to retain its subject
which is great, but I of course need to retain my ControlEventPassthroughSubject
too. I also need to not cause a retain-cycle between the two, so the ControlEventPassthroughSubject
has an unowned
reference to its subject. This all feels a bit wonky, and I assume there's a better way, but I've not come up with anything else that doesn't involve re-implementing something like a PassthroughSubject
that can also receive UIControl.Event
callbacks.
Separately, I find this a bit odd:
// elsewhere...
self.cancelable = Publishers.ControlForEvent(control: self.button, event: .touchUpInside).sink {
print("tapped button:", $0)
}
The Cancelable
I get here does not cancel when it is released. I need to explicitly call cancel
on it. This means I either need to implement a deinit
method on any classes I have that hold on to Cancelable
s to cancel()
them, or I need to implement something like RxSwift
's DisposeBag
that can cancel()
everything on its own deinit
. It seems a bit clunky as is, but as with the first question above I hope maybe I'm missing something!
Thanks for taking a look!