Convert a Swift Delegate to RxSwift Observables

Daniel Tartaglia
4 min readJul 20, 2021

Most of the delegate protocols in the UIKit library already have reactive methods designed to convert the delegate into Observables. For those types in other libraries that haven’t been converted or if you are having to convert legacy code, you can create your own delegate proxy using the tools contained in the RxCocoa library.

The way you convert the delegate into Observables depends very much on the kinds of delegate methods and some of them simply can’t be converted. Not all methods can be converted; sometimes the delegate method requires that a calculation be made on the spot in order to return a value and Observables don’t allow that.

The Basics

First let’s cover the basic structure of every delegate proxy. Given a class that uses the delegate and a protocol that defines the delegate from some library:

class Thing: NSObject {
weak var delegate: ThingDelegate? = nil
}
@objc
protocol ThingDelegate: AnyObject {
// some number of methods
}

You first have to create the Proxy class. It’s easier to do this if your Thing type can conform to RxCocoa’s HasDelegate protocol, but it’s not a problem if you can’t make that work, it just means implementing a couple of extra methods in your proxy and the compiler will insert the stubs for you.

Here’s the basic shell of every delegate proxy class:

class ThingDelegateProxy
: DelegateProxy<Thing, ThingDelegate>
, DelegateProxyType
, ThingDelegate {
init(parentObject: Thing) {
super.init(
parentObject: parentObject,
delegateProxy: ThingDelegateProxy.self
)
}
public static func registerKnownImplementations() {
self.register { ThingDelegateProxy(parentObject: $0) }
}
}
extension Reactive where Base: Thing {
var delegate: ThingDelegateProxy {
return ThingDelegateProxy.proxy(for: base)
}
}

Using the above as a base, everything else that is needed will depend on what methods are on the delegate. Are they optional or required? Do they pass a value to the delegate as one or more parameters or do they expect the delegate to return a value? Let’s try to cover each use case.

Optional Method, Passes a Parameter

The most common use case is that the method is optional and it passes one or more parameters to the delegate:

@objc
protocol ThingDelegate: AnyObject {
@objc optional func thing(
_ thing: Thing,
hasDoneSomething param1: String,
with value: Int
)
}

In cases where the delegate receives the Thing, you can ignore the thing passed back to the delegate because the Rx system creates a different delegate object for each thing. The other value(s) passed into the method should be included in the Observable for use. Simply add something like this to your proxy and you are done:

var hasDoneSomething: Observable<(param1: String, value: Int)> {
delegate.methodInvoked(
#selector(ThingDelegate.thing(_:hasDoneSomething:with:))
)
.map { ($0[1] as! String, $0[2] as! Int) }
}

Required Method, Passes a Parameter

The next use case is when the method is required.

@objc
protocol ThingDelegate: AnyObject {
func thing(
_ thing: Thing,
hasDoneSomething param1: String,
with value: Int
)
}

In this case the function may or may not be an @objc method; it doesn’t matter your code will be the same. When confronted with a required method, your delegate proxy must implement the method and it should use a Subject to forward the value(s) on to the Reactive operator.

So you add this to the proxy itself:

func thing(
_ thing: Thing,
hasDoneSomething param1: String,
with value: Int
) {
hasDoneSomething.onNext((param1, value))
}
fileprivate let hasDoneSomething =
PublishSubject<(param1: String, value: Int)>()

And add this to the reactive extension:

var hasDoneSomething: Observable<(param1: String, value: Int)> {
delegate.hasDoneSomething
}

Method Returns a Value

This use case requires you to return a value:

protocol ThingDelegate: AnyObject {
func thingNeedsSomething(_ thing: Thing) -> String?
}

In this case, it doesn’t matter whether the method is optional or not, you have to handle it the same way. These sorts of methods must be implemented in the delegate proxy and need to be converted into an Observer instead of an Observable.

Here’s what needs to go in your proxy. Notice that you have to provide a default value. Be sure that is the value that the caller uses if the method isn’t implemented.

func thingNeedsSomething(_ thing: Thing) -> String? {
relay.value
}
fileprivate let needsSomethingRelay =
BehaviorRelay<String?>(value: nil)

And then in the Reactive extension put this:

var needsSomething: Binder<String?> {
Binder(delegate) { del, value in
del.needsSomethingRelay.accept(value)
}
}

Method both Passes a Parameter and Returns a Value

Here’s where it gets tricky and you may not be able to provide an Observable. You can only do it if you know ahead of time all the values that will be passed in.

For example if the method looks like this:

protocol ThingDelegate: AnyObject {
func thingNeedsSomething(
_ thing: Thing,
for item: String) -> Int
}

You would have to know all the possible values that might be passed to item and what you would want to return in order to implement this method as an Observer.

Add this code to your proxy:

func thingNeedsSomething(_ thing: Thing, for item: String) -> Int {
relay.value[item] ?? 0
}
fileprivate let relay = BehaviorRelay<[String: Int]>(value: [:])

And then in your reactive extension add this code:

var needsSomething: Binder<[String: Int]> {
Binder(delegate) { del, value in
del.relay.accept(value)
}
}

Epilogue

Creating a proxy for your delegates can be a bit of a pain sometimes but they really clean up the point of use and make everything much easier to understand. The next time you are confronted with a new delegate, instead of casting about on the internet trying to find a solution, you now have the knowledge to make the proxy yourself!

--

--

Daniel Tartaglia

I started programming as a hobby in the late ’70s and professionally in the late ’90s. I’ve been writing iOS apps since 2010 and using RxSwift since 2015.