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

4

16

I'm subclassing UIScrollView to add some features such as double tap to zoom and an image property for gallery purposes. But in order to do the image part my subclass has to be its own delegate and implement the viewForZoomingInScrollView.

But then when someone uses my scroll view subclass, they might like to get delegate notifications as well to see scrollViewDidScroll or what have you.

In Swift, how do I get both of these?

Afire answered 16/11, 2014 at 3:52 Comment(3)
Do you know how to address this problem in ObjC and want to port it to Swift?Hypochondriasis
If not, try to do so with the solution provided in this answer https://mcmap.net/q/95152/-how-to-subclass-uiscrollview-and-make-the-delegate-property-privateHypochondriasis
I can't figure out a Swift implementation.Afire
W
24

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")
    }
}
Wicklund answered 17/11, 2014 at 4:33 Comment(4)
Nice catch. I remember trying the same but got an strange error(don't remember which one) overriding respondsToSelector.Hypochondriasis
In Swift 1.2, brackets are needed as following: super.respondsToSelector(aSelector) || (_userDelegate?.respondsToSelector(aSelector) == true)Birkenhead
Awesome answer for proper handling of delegation forwarding. Totally saved me for my custom implementation of a UITableView! :)Epizootic
Tried this pattern on UITextView, it does not work. Overriding var delegate breaks itTelemeter
Y
7

Swift 4+ version of rintaro's excellent answer:

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

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

        override func forwardingTarget(for aSelector: Selector!) -> Any? {
            if _userDelegate?.responds(to: aSelector) == true {
                return _userDelegate
            }

            return super.forwardingTarget(for: aSelector)
        }

        //This function is just a demonstration, it can be replaced/removed.
        func scrollViewDidScroll(_ scrollView: UIScrollView) {
            (scrollView as? MyScrollView)?.didScroll()

            _userDelegate?.scrollViewDidScroll?(scrollView)
        }
    }

    fileprivate let _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 {
            _delegateProxy._userDelegate = newValue
        }
    }

    func didScroll() {
        print("didScroll")
    }
}
Yet answered 25/6, 2019 at 15:56 Comment(0)
C
4

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

Connotation answered 29/8, 2017 at 19:48 Comment(0)
H
3

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
Hypochondriasis answered 17/11, 2014 at 2:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.