I recently found myself in a situation where I needed to listen out for changes to a UIScrollView of which I couldn’t set the delegate. My situation was with the underlying UIScrollView of a UIWebView instance, but there are other situations where you might want to receive delegate method calls from an object for which you are not allowed to set the delegate.
The problem is that UIWebView exposes its underlying UIScrollView, but it would be bad etiquette to set its delegate, because you are not the owner of the UIScrollView instance and you don’t know the implementation of UIWebView.
So, the question is: how can we listen out for the delegate messages from a UIScrollView, when we are not acting as the delegate?
One solution would be to use KVO. But it wouldn’t be possible to implement
-scrollViewWillEndDragging:withVelocity:targetContentOffset: using KVO.
So I propose adding a
secondaryDelegate property in a category on UIScrollView. This category will implement the
-setSecondaryDelegate: method, which can be called from the app (from a view controller, say).
1 2 3 4 5
So for example, my view controller can set itself as the “delegate” of its web view’s UIScrollView as follows:
1 2 3 4 5 6
By setting the
secondaryDelegate property, the category’s setter method actually overrides the
delegate property with a
delegateProxy object. This proxy object holds a weak reference to the original (overridden) delegate and to the secondary delegate provided.
The delegate proxy is responsible for forwarding messages to the relevant delegates. The proxy holds a weak reference to each delegate. This means it is perfectly acceptable for the UIScrollView to hold a strong reference to the proxy by way of associated object (explained in more detail later).
The proxy object is a subclass of NSProxy and conforms to the UIScrollViewDelegate protocol, so it must implement the following methods:
We’ll also implement the
-respondsToSelector: method, because we know our proxy will be used as the delegate of a UIScrollView, which will check if the delegate responds to each selector before calling that selector.
To determine the method signature for a selector, we can just ask each delegate and return when we find one that can provide what we need.
1 2 3 4 5
To determine if the proxy can respond to a selector, again we just check each delegate and return
YES if one of them can respond.
Before calling one of its optional delegate methods the UIScrollView (or anyone else for that matter) asks its delegate if it can respond to the selector. If it replies with
YES, then it will send the message. But because we have set the
delegate property to our
delegateProxy object, this message will arrive as an invocation in the
We should implement this method as follows in order to forward the invocation to each delegate in turn (if it can respond to the selector).
1 2 3 4 5 6 7 8 9 10 11 12
The UIScrollView Category
For completeness, I’ve added the code for the category, with comments below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
delegateProxyproperty is added to the private interface extension. The fact that this category uses a proxy is an implementation detail and should therefore not be exposed in the public interface.
- The setter method for the
secondaryDelegateproperty will create the
delegateProxyobject if it doesn’t already exist and assign it to the
delegateProxyproperty. Next we the
primaryDelegateto the original delegate held in
self.delegate. Then we set the
delegateProxy.secondaryDelegateand override the object held in
self.delegate. (Notice how we only call
allocon the proxy class - this is because instances of NSProxy do not by default respond to
- We should provide the getter method for the object. It is just a wrapper around getting it from
- We hold a strong reference to the
delegateProxyobject by using an associated object. The strong reference is fine, because it only holds weak references to each delegate. (Notice the use of
@selector(delegateProxy)as the key for the associated object. This is fine because the pointer returned is unique.)
This solution is clean and robust. It doesn’t involve method swizzling - only message forwarding using a proxy object, which is fine. In fact the only reason we have to import
<objc/runtime.h> is to use associated objects.
The public interface is clean - it only provides a single extra property for UIScrollView - the
The only downside to this is that the original delegate must have been set (if it ever will be set) before you set the secondary delegate. Or rather, you cannot set the
delegate property after setting the
secondaryDelegate. This is because setting the
secondaryDelegate overwrites the value of the
delegate property. I thought about a solution that adds an observer to listen for changes to the
delegate property, but removing the observer on deallocation was not trivial, so I settled on the solution described above, which will not allow the delegate to be set after having set the secondary delegate.