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")
}
}
How to subclass UIScrollView and make the delegate property private
There is a problem with making MySubclass
its own delegate. Presumably you don't want to run custom code for all of the UIScrollViewDelegate
methods, but you have to forward the messages to the user-provided delegate whether you have your own implementation or not. So you could try to implement all of the delegate methods, with most of them just forwarding like this:
- (void)scrollViewDidZoom:(UIScrollView *)scrollView {
[self.myOwnDelegate scrollViewDidZoom:scrollView];
}
The problem here is that sometimes new versions of iOS add new delegate methods. For example, iOS 5.0 added scrollViewWillEndDragging:withVelocity:targetContentOffset:
. So your scrollview subclass won't be future-proof.
The best way to handle this is to create a separate, private object that just acts as your scrollview's delegate, and handles forwarding. This dedicated-delegate object can forward every message it receives to the user-provided delegate, because it only receives delegate messages.
Here's what you do. In your header file, you only need to declare the interface for your scrollview subclass. You don't need to expose any new methods or properties, so it just looks like this:
MyScrollView.h
@interface MyScrollView : UIScrollView
@end
All the real work is done in the .m
file. First, we define the interface for the private delegate class. Its job is to call back into MyScrollView
for some of the delegate methods, and to forward all messages to the user's delegate. So we only want to give it methods that are part of UIScrollViewDelegate
. We don't want it to have extra methods for managing a reference to the user's delegate, so we'll just keep that reference as an instance variable:
MyScrollView.m
@interface MyScrollViewPrivateDelegate : NSObject <UIScrollViewDelegate> {
@public
id<UIScrollViewDelegate> _userDelegate;
}
@end
Next we'll implement MyScrollView
. It needs to create an instance of MyScrollViewPrivateDelegate
, which it needs to own. Since a UIScrollView
doesn't own its delegate, we need an extra, strong reference to this object.
@implementation MyScrollView {
MyScrollViewPrivateDelegate *_myDelegate;
}
- (void)initDelegate {
_myDelegate = [[MyScrollViewPrivateDelegate alloc] init];
[_myDelegate retain]; // remove if using ARC
[super setDelegate:_myDelegate];
}
- (id)initWithFrame:(CGRect)frame {
if (!(self = [super initWithFrame:frame]))
return nil;
[self initDelegate];
return self;
}
- (id)initWithCoder:(NSCoder *)aDecoder {
if (!(self = [super initWithCoder:aDecoder]))
return nil;
[self initDelegate];
return self;
}
- (void)dealloc {
// Omit this if using ARC
[_myDelegate release];
[super dealloc];
}
We need to override setDelegate:
and delegate:
to store and return a reference to the user's delegate:
- (void)setDelegate:(id<UIScrollViewDelegate>)delegate {
_myDelegate->_userDelegate = delegate;
// Scroll view delegate caches whether the delegate responds to some of the delegate
// methods, so we need to force it to re-evaluate if the delegate responds to them
super.delegate = nil;
super.delegate = (id)_myDelegate;
}
- (id<UIScrollViewDelegate>)delegate {
return _myDelegate->_userDelegate;
}
We also need to define any extra methods that our private delegate might need to use:
- (void)myScrollViewDidEndDecelerating {
// do whatever you want here
}
@end
Now we can finally define the implementation of MyScrollViewPrivateDelegate
. We need to explicitly define each method that should contain our private custom code. The method needs to execute our custom code, and forward the message to the user's delegate, if the user's delegate responds to the message:
@implementation MyScrollViewPrivateDelegate
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
[(MyScrollView *)scrollView myScrollViewDidEndDecelerating];
if ([_userDelegate respondsToSelector:_cmd]) {
[_userDelegate scrollViewDidEndDecelerating:scrollView];
}
}
And we need to handle all of the other UIScrollViewDelegate
methods that we don't have custom code for, and all of those messages that will be added in future versions of iOS. We have to implement two methods to make that happen:
- (BOOL)respondsToSelector:(SEL)selector {
return [_userDelegate respondsToSelector:selector] || [super respondsToSelector:selector];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
// This should only ever be called from `UIScrollView`, after it has verified
// that `_userDelegate` responds to the selector by sending me
// `respondsToSelector:`. So I don't need to check again here.
[invocation invokeWithTarget:_userDelegate];
}
@end
Catching a delegate method call in the originating subclass
This will take some time but you can achieve what you want.
//EDITED
I have added implementation of the method forwardInvocation:
. This method is invoked on object when object doesn't recognize message which was sent to it.
In our case when some of the UIScrollView
delegate's methods will be called and our subclass and it doesn't implement called method, forwardInvocation
will be called. This method checks whether called selector is part of the UIScrollViewDelegate
protocol. If yes and if 'true' delegate of our class responds to this selector we forward invocation of called selector to our custom delegate.
That being said, using this solution you don't have to implement all methods of UIScrollViewDelegate
protocol in your subclass. Execution of the methods which won;t be implemented in subclass of the ScrollView
will be forwarded to 'true' delegate of our subclass.
Example:
#import <objc/runtime.h>
@protocol YourSubclassDelegate<UIScrollViewDelegate>
@end
@interface YourSubclass ()
@property (nonatomic, weak) id<YourSubclassDelegate> delegate;
@end
@implementation
//override delegate setter
- (void)setDelegate:(UIScrollViewDelegate)delegate
{
self.customDelegate = delegate;
[super setDelegate:self];
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
//do something you want
if ([self.customDelegate respondsToSelector:@selector(scrollViewDidScroll:)]) {
[self.customDelegate scrollViewDidScroll:self];
}
}
- (id)forwardingTargetForSelector:(SEL)selector
{
struct objc_method_description methodDescription = protocol_getMethodDescription(@protocol(UIScrollViewDelegate), selector, YES, YES);
if(NULL != methodDescription.name) {
if ([self.customDelegate respondsToSelector:selector]) {
return self.customDelegate;
}
}
return nil;
}
@end
Swift, implement same delegate two times (without multicast)
It is actually possible using a Proxy delegate. However not really recommended.
In Swift, how do I have a UIScrollView subclass that has an internal and external delegate?
How to have a UIScrollView scroll and have a gesture recognizer?
Make a subclass of UIScrollView
. Add this method in your new subclass
- (BOOL)gestureRecognizer:(UIPanGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UISwipeGestureRecognizer *)otherGestureRecognizer
{
return YES;
}
Make your scrollView class to your new scrollview subclass.
Nested UIScrollViews scrolling simultaneously
The glitchy behavior was happening when the views were dragged out of the content area, released, and then tapped/dragged again before the scroll views bounced back. This could happen, for example, when the view was scrolled by several small swipes. One of the scroll views would get confused and try to decelerate (bounce) while simultaneously being dragged, causing it to jitter back and forth between the origin and where it had been dragged to.
I was able to fix this by reversing the nesting of the scroll views (paging view inside of the vertical scrolling view) and by adding the delegate to the paging view's UIPanGestureRecognizer instead of to the scrolling view's gesture. Now it scrolls naturally as if it was a single scroll view while still conforming to paging only in the horizontal direction. I don't think it was intended for scroll views to be tricked into scrolling simultaneously like this, so I'm not sure if the original glitchy behavior was the result of a bug, or just a consequence of doing something unintended.
Related Topics
Best Practice for Swift Methods That Can Return or Error
Curl Through Nstask Not Terminating If a Pipe Is Present
Check If Variable Is an Optional, and What Type It Wraps
Pass C Function Callback in Swift
Ios8 Custom Keyboard - Copy & Paste to Uipasteboard
Are Lazy Vars in Swift Computed More Than Once
In Swift, What Does It Mean for Protocol to Inherit from Class Keyword
iOS Charts 3.0 - Align X Labels (Dates) with Plots
Using Stringbyreplacingcharactersinrange in Swift
Scncamera Limit Arcball Rotation
How to Execute Code Once and Only Once in Swift
Does Swift Have a Null Coalescing Operator and If Not, What Is an Example of a Custom Operator
Listing All Files in a Folder Recursively with Swift
Why in Swift We Cannot Adopt a Protocol Without Inheritance a Class from Nsobject
Initializer for Conditional Binding Must Have Optional Type, Not 'String'