В Swift, как у меня есть подкласс UIScrollView, который имеет внутренний и внешний делегат?

Я подклассифицирую UIScrollView, чтобы добавить некоторые функции, такие как двойное нажатие для масштабирования и свойство изображения для целей галереи. Но для того, чтобы сделать часть изображения, мой подкласс должен быть его собственным делегатом и реализовать viewForZoomingInScrollView.

Но тогда, когда кто-то использует мой подкласс просмотра прокрутки, они могут также получать уведомления делегата, а также видеть scrollViewDidScroll или что у вас есть.

В Swift, как мне получить оба этих?

Ответ 1

Вот быстрая версия этого шаблона:

Хотя forwardInvocation: отключен в Swift, мы все равно можем использовать 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")
    }
}

Ответ 2

Здесь простая рабочая версия игровой площадки в Swift 3, которая действует исключительно как наблюдатель, а не только как перехватчик, как и другие ответы здесь.

Отличие заключается в том, что у делегата представления прокрутки оригинала должны быть все его методы делегирования, которые называются нормальными или их уволенными другим делегатом.

(Вы можете скопировать/вставить это в игровое поле и запустить его для тестирования)

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 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 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)

Отпечатает

прокси сделал прокрутку

просмотреть оригинал прокрутил метод делегирования под названием

удалил наблюдателя

просмотреть оригинал прокрутил метод делегирования под названием

просмотреть оригинал прокрутил метод делегирования под названием

Ответ 3

Я не знаю о 100% -ном решении Swift для этого. Принимая этот ответ ObjC к той же проблеме и пытаясь перенести его в Swift, оказывается, что это невозможно, так как NSInvocation недоступен в Swift.

Что мы можем сделать, так это реализовать предложенный MyScrollViewPrivateDelegate в ObjC (не забудьте импортировать его в заголовочный файл моста) и подкласс вида прокрутки в Swift, как показано ниже:

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

UIScrollView уже определяет свойство delegate, что означает, что любой подкласс UIScrollView не сможет определить свойство с именем delegate.

Ниже CustomScrollView имеет экземпляр UIScrollView вместо подкласса UIScrollView. Это позволяет CustomScrollView определять свойство delegate без конфликта с суперклассом.

protocol CustomScrollViewDelegate: class {
    func scrollViewDidScroll()
}

class CustomScrollView: UIView, UIScrollViewDelegate {
    var scrollView: UIScrollView = UIScrollView()
    weak var delegate: CustomScrollViewDelegate? // External delegate

    override init (frame : CGRect) {
        super.init(frame : frame)
        setup()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    func setup() {
        scrollView.delegate = self // Internal delegate
        addSubview(scrollView)
    }

    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        delegate?.scrollViewDidScroll() // External delegation
    }
}

Ответ 5

Swift 4+ версия Rintaro отличный ответ:

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")
    }
}