In Swift, how do I have a UIScrollView subclass th

2019-01-22 15:31发布

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?

3条回答
戒情不戒烟
2楼-- · 2019-01-22 15:37

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")
    }
}
查看更多
趁早两清
3楼-- · 2019-01-22 15:51

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
查看更多
劳资没心,怎么记你
4楼-- · 2019-01-22 15:51

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

查看更多
登录 后发表回答