In Swift, how do I have a UIScrollView subclass that has an internal and external delegate?

Here is a Swift version of this pattern:

Although forwardInvocation: is disabled in Swift, we can still use forwardingTargetForSelector:

class MyScrollView: UIScrollView {

    class _DelegateProxy: NSObject, UIScrollViewDelegate {
        weak var _userDelegate: UIScrollViewDelegate?

        override func respondsToSelector(aSelector: Selector) -> Bool {
            return super.respondsToSelector(aSelector) || _userDelegate?.respondsToSelector(aSelector) == true
        }

        override func forwardingTargetForSelector(aSelector: Selector) -> AnyObject? {
            if _userDelegate?.respondsToSelector(aSelector) == true {
                return _userDelegate
            }
            else {
                return super.forwardingTargetForSelector(aSelector)
            }
        }

        func viewForZoomingInScrollView(scrollView: MyScrollView) -> UIView? {
            return scrollView.viewForZooming()
        }

        // Just a demo. You don't need this.
        func scrollViewDidScroll(scrollView: MyScrollView) {
            scrollView.didScroll()
            _userDelegate?.scrollViewDidScroll?(scrollView)
        }
    }

    private var _delegateProxy =  _DelegateProxy()

    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        super.delegate = _delegateProxy
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        super.delegate = _delegateProxy
    }

    override var delegate:UIScrollViewDelegate? {
        get {
            return _delegateProxy._userDelegate
        }
        set {
            self._delegateProxy._userDelegate = newValue;
            /* It seems, we don't need this anymore.
            super.delegate = nil
            super.delegate = _delegateProxy
            */
        }
    }

    func viewForZooming() -> UIView? {
        println("self viewForZooming")
        return self.subviews.first as? UIView // whatever
    }

    func didScroll() {
        println("self didScroll")
    }
}

Here's a simple working Playground version in Swift 3 that acts purely as an observer rather than only as an interceptor like the other answers here.

The distinction is that the original scroll view delegate should have all of its delegate methods called like normal versus them being hijacked by another delegate.

(You can copy/paste this into a playground and run it to test)

import UIKit

final class ScrollViewObserver: NSObject, UIScrollViewDelegate {

    // MARK: - Instantiation

    init(scrollView: UIScrollView) {
        super.init()

        self.scrollView = scrollView
        self.originalScrollDelegate = scrollView.delegate
        scrollView.delegate = self
    }

    deinit {
        self.remove()
    }

    // MARK: - API

    /// Removes ourselves as an observer, resetting the scroll view's original delegate
    func remove() {
        self.scrollView?.delegate = self.originalScrollDelegate
    }

    // MARK: - Private Properties

    fileprivate weak var scrollView: UIScrollView?
    fileprivate weak var originalScrollDelegate: UIScrollViewDelegate?

    // MARK: - Forwarding Delegates

    /// Note: we forward all delegate calls here since Swift does not support forwardInvocation: or NSProxy

    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        // Run any custom logic or send any notifications here
        print("proxy did scroll")

        // Then, forward the call to the original delegate
        self.originalScrollDelegate?.scrollViewDidScroll?(scrollView)
    }

    func scrollViewDidZoom(_ scrollView: UIScrollView) {
        self.originalScrollDelegate?.scrollViewDidZoom?(scrollView)
    }

    func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
        self.originalScrollDelegate?.scrollViewWillBeginDragging?(scrollView)
    }

    func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
        self.originalScrollDelegate?.scrollViewWillEndDragging?(scrollView, withVelocity: velocity, targetContentOffset: targetContentOffset)
    }

    func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
        self.originalScrollDelegate?.scrollViewDidEndDragging?(scrollView, willDecelerate: decelerate)
    }

    func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) {
        self.originalScrollDelegate?.scrollViewWillBeginDecelerating?(scrollView)
    }

    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
        self.originalScrollDelegate?.scrollViewDidEndDecelerating?(scrollView)
    }

    func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
        self.originalScrollDelegate?.scrollViewDidEndScrollingAnimation?(scrollView)
    }

    func viewForZooming(in scrollView: UIScrollView) -> UIView? {
        return self.originalScrollDelegate?.viewForZooming?(in: scrollView)
    }

    func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) {
        self.originalScrollDelegate?.scrollViewWillBeginZooming?(scrollView, with: view)
    }

    func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
        self.originalScrollDelegate?.scrollViewDidEndZooming?(scrollView, with: view, atScale: scale)
    }

    func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool {
        return self.originalScrollDelegate?.scrollViewShouldScrollToTop?(scrollView) == true
    }

    func scrollViewDidScrollToTop(_ scrollView: UIScrollView) {
        self.originalScrollDelegate?.scrollViewDidScrollToTop?(scrollView)
    }

}

final class TestView: UIView, UIScrollViewDelegate {

    let scrollView = UIScrollView()
    fileprivate(set) var scrollObserver: ScrollViewObserver?

    required init() {
        super.init(frame: .zero)

        self.scrollView.delegate = self
        self.scrollObserver = ScrollViewObserver(scrollView: self.scrollView)
    }

    required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }

    public func scrollViewDidScroll(_ scrollView: UIScrollView) {
        print("view's original did scroll delegate method called")
    }

}

let testView = TestView()
testView.scrollView.setContentOffset(CGPoint(x: 0, y: 100), animated: true)
testView.scrollObserver?.remove()
print("removed the observer")
testView.scrollView.setContentOffset(CGPoint(x: 0, y: 200), animated: true)
testView.scrollView.setContentOffset(CGPoint(x: 0, y: 300), animated: true)

This prints

proxy did scroll

view's original did scroll delegate method called

removed the observer

view's original did scroll delegate method called

view's original did scroll delegate method called


I don't know about any 100% Swift solution for this. Taking this ObjC answer to the same problem, and trying to port it to Swift it turns out that is not possible since NSInvocation is not available in Swift.

What we can do is to implement the suggested MyScrollViewPrivateDelegate in ObjC(don't forget to import it in the bridging header file) and the scroll view subclass in Swift like the following:

MyScrollView.swift

import UIKit

class MyScrollView: UIScrollView {

    private let myDelegate = MyScrollViewPrivateDelegate()

    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        super.delegate = myDelegate
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        super.delegate = myDelegate
    }

    override var delegate: UIScrollViewDelegate? {
        set {
            myDelegate.userDelegate = newValue
            super.delegate = nil
            super.delegate = myDelegate
        }

        get {
            return myDelegate.userDelegate
        }
    }

    func viewForZooming() -> UIView {
        return UIView()// return whatever you want here...
    }
}

MyScrollViewPrivateDelegate.h

#import <UIKit/UIKit.h>

@interface MyScrollViewPrivateDelegate : NSObject <UIScrollViewDelegate>

@property (weak, nonatomic) id<UIScrollViewDelegate> userDelegate;

@end

MyScrollViewPrivateDelegate.m

#import "MyScrollViewPrivateDelegate.h"
#import "YOUR_MODULE-Swift.h"

@implementation MyScrollViewPrivateDelegate

- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
{
    // you could check if the user delegate responds to viewForZoomingInScrollView and call it instead...
    return [(MyScrollView *)scrollView viewForZooming];
}

- (BOOL)respondsToSelector:(SEL)selector 
{
    return [_userDelegate respondsToSelector:selector] || [super respondsToSelector:selector];
}

- (void)forwardInvocation:(NSInvocation *)invocation 
{
    [invocation invokeWithTarget:_userDelegate];
}

@end