web-dev-qa-db-ja.com

UIScrollViewをサブクラス化してデリゲートプロパティをプライベートにする方法

これが私が達成したいことです:

UIScrollViewをサブクラス化して、追加の機能を持たせたい。このサブクラスはスクロールに反応できるはずなので、次のようなイベントを受信するには、デリゲートプロパティをselfに設定する必要があります。

- (void) scrollViewDidEndDecelerating:(UIScrollView *)scrollView { ... }

一方、他のクラスも、基本のUIScrollViewクラスを使用していたように、これらのイベントを受信できるはずです。

だから私はその問題を解決する方法についてさまざまなアイデアを持っていましたが、これらすべてが私を完全に満足させるわけではありません:(

私の主なアプローチは、次のような独自のデリゲートプロパティを使用することです。

@interface MySubclass : UIScrollView<UIScrollViewDelegate>
@property (nonatomic, assign) id<UIScrollViewDelegate> myOwnDelegate;
@end

@implementation MySubclass
@synthesize myOwnDelegate;

- (id) initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        self.delegate = self;
    }
    return self;
}

// Example event
- (void) scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    // Do something custom here and after that pass the event to myDelegate
    ...
    [self.myOwnDelegate scrollViewDidEndDecelerating:(UIScrollView*)scrollView];
}
@end

このようにして、継承されたscrollviewがスクロールを終了したときに、私のサブクラスは特別なことを行うことができますが、それでも外部デリゲートにイベントを通知します。これまでのところ機能します。ただし、このサブクラスを他の開発者が利用できるようにしたいので、基本クラスのデリゲートプロパティへのアクセスを制限したいと思います。これは、サブクラスによってのみ使用される必要があるためです。ヘッダーファイルで問題にコメントを付けても、他の開発者が基本クラスのデリゲートプロパティを直感的に使用している可能性が高いと思います。誰かがデリゲートプロパティを変更した場合、サブクラスは本来の動作を実行せず、現時点ではそれを防ぐために何もできません。そして、それは私がそれを解決する方法の手がかりを持っていないポイントです。

私が試したのは、デリゲートプロパティをオーバーライドして、次のように読み取り専用にすることです。

@interface MySubclass : UIScrollView<UIScrollViewDelegate>
...
@property (nonatomic, assign, readonly) id<UIScrollViewDelegate>delegate;
@end

@implementation MySubclass
@property (nonatomic, assign, readwrite) id<UIScrollViewDelegate>delegate;
@end

警告が表示されます

"Attribute 'readonly' of property 'delegate' restricts attribute 'readwrite' of property inherited from 'UIScrollView'

私は明らかにここでリスコフの置換原理に違反しているので、悪い考えです。

次に試す->次のようにデリゲートセッターをオーバーライドしようとします。

...
- (void) setDelegate(id<UIScrollViewDelegate>)newDelegate {
    if (newDelegate != self) self.myOwnDelegate = newDelegate;
    else _delegate = newDelegate; // <--- This does not work!
}
...

コメントしたように、_delegate ivarが見つからなかったように見えるため、この例はコンパイルされません!?だから私はUIScrollViewのヘッダーファイルを調べて、これを見つけました:

@package
    ...
    id           _delegate;
...

@packageディレクティブは、_delegateivarのアクセスをフレームワーク自体からのみアクセスできるように制限します。したがって、_delegate ivarを設定する場合は、合成されたセッターを使用する必要があります。私はそれをオーバーライドする方法を見つけることができません:(しかし、私はこれを回避する方法がないことを信じることができません、多分私は木のために木を見ることができません。

この問題を解決するためのヒントをいただければ幸いです。


解決:

@robmayoffのソリューションで動作するようになりました。すぐ下でコメントしたように、scrollViewDidScroll:呼び出しに問題がありました。私はついに問題が何であるかを知りました、私でさえこれがなぜそうなのか理解していません:/

特別代議員を設定した瞬間:

- (id) initWithFrame:(CGRect)frame {
    ...
    _myDelegate = [[[MyPrivateDelegate alloc] init] autorelease];
    [super setDelegate:_myDelegate]; <-- Callback is invoked here
}

_myDelegateへのコールバックがあります。デバッガーはで壊れます

- (BOOL) respondsToSelector:(SEL)aSelector {
    return [self.userDelegate respondsToSelector:aSelector];
}

「scrollViewDidScroll:」セレクターを引数として使用します。

現時点で面白いのは、self.userDelegateがまだ設定されておらず、nilを指しているため、戻り値はNOです。これにより、scrollViewDidScroll:メソッドが後で起動されないようです。メソッドが実装されているかどうかは事前チェックのように見えます。失敗した場合は、後でuserDelegateプロパティを設定しても、このメソッドはまったく起動されません。他のほとんどのデリゲートメソッドにはこの事前チェックがないため、これがなぜそうなのかわかりません。

したがって、これに対する私の解決策は、PrivateDelegatesetDelegateメソッドで[supersetDelegate ...]メソッドを呼び出すことです。これは、userDelegateメソッドが設定されていると確信している場所です。

だから私はこの実装スニペットで終わるでしょう:

MyScrollViewSubclass.m

- (void) setDelegate:(id<UIScrollViewDelegate>)delegate {
    self.internalDelegate.userDelegate = delegate;  
    super.delegate = self.internalDelegate;
}

- (id) initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        self.internalDelegate = [[[MyScrollViewPrivateDelegate alloc] init] autorelease];
        // Don't set it here anymore
    }
    return self;
}

残りのコードはそのままです。 setDelegateメソッドを少なくとも1回呼び出す必要があるため、この回避策にはまだ満足していませんが、非常にハッキーな感じがしますが、今のところ私のニーズには対応しています:/

誰かがそれを改善する方法についてアイデアを持っているなら、私はそれをいただければ幸いです。

あなたの例をありがとう@rob!

26
Mexyn

MySubclassを独自のデリゲートにすることには問題があります。おそらく、UIScrollViewDelegateメソッドのallのカスタムコードを実行する必要はありませんが、独自の実装があるかどうかに関係なく、ユーザー指定のデリゲートにメッセージを転送する必要があります。したがって、すべてのデリゲートメソッドの実装を試みることができ、それらのほとんどは次のように転送するだけです。

- (void)scrollViewDidZoom:(UIScrollView *)scrollView {
    [self.myOwnDelegate scrollViewDidZoom:scrollView];
}

ここでの問題は、iOSの新しいバージョンが新しいデリゲートメソッドを追加する場合があることです。たとえば、iOS5.0ではscrollViewWillEndDragging:withVelocity:targetContentOffset:が追加されました。したがって、scrollviewサブクラスは将来に対応できません。

これを処理する最良の方法は、スクロールビューのデリゲートとして機能し、転送を処理する個別のプライベートオブジェクトを作成することです。 onlyデリゲートメッセージを受信するため、この専用デリゲートオブジェクトは、受信したeveryメッセージをユーザー指定のデリゲートに転送できます。

これがあなたの仕事です。ヘッダーファイルでは、scrollviewサブクラスのインターフェイスを宣言するだけで済みます。新しいメソッドやプロパティを公開する必要がないため、次のようになります。

MyScrollView.h

@interface MyScrollView : UIScrollView
@end

実際の作業はすべて.mファイルで行われます。まず、プライベートデリゲートクラスのインターフェイスを定義します。その仕事は、いくつかのデリゲートメソッドのためにMyScrollViewにコールバックし、allメッセージをユーザーのデリゲートに転送することです。したがって、UIScrollViewDelegateの一部であるメソッドのみを提供したいと思います。ユーザーのデリゲートへの参照を管理するための追加のメソッドを持たせたくないので、その参照をインスタンス変数として保持します。

MyScrollView.m

@interface MyScrollViewPrivateDelegate : NSObject <UIScrollViewDelegate> {
@public
    id<UIScrollViewDelegate> _userDelegate;
}
@end

次に、MyScrollViewを実装します。所有する必要のあるMyScrollViewPrivateDelegateのインスタンスを作成する必要があります。 UIScrollViewはデリゲートを所有していないため、このオブジェクトへの追加の強力な参照が必要です。

@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];
}

ユーザーのデリゲートへの参照を格納して返すには、setDelegate:および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;
}

また、プライベートデリゲートが使用する必要がある可能性のある追加のメソッドを定義する必要があります。

- (void)myScrollViewDidEndDecelerating {
    // do whatever you want here
}

@end

これで、最終的にMyScrollViewPrivateDelegateの実装を定義できます。プライベートカスタムコードを含める必要がある各メソッドを明示的に定義する必要があります。ユーザーのデリゲートがメッセージに応答した場合、メソッドはカスタムコードを実行し、メッセージをユーザーのデリゲートに転送する必要があります。

@implementation MyScrollViewPrivateDelegate

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    [(MyScrollView *)scrollView myScrollViewDidEndDecelerating];
    if ([_userDelegate respondsToSelector:_cmd]) {
        [_userDelegate scrollViewDidEndDecelerating:scrollView];
    }
}

また、カスタムコードがない他のすべてのUIScrollViewDelegateメソッドと、iOSの将来のバージョンで追加されるすべてのメッセージを処理する必要があります。これを実現するには、2つの方法を実装する必要があります。

- (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
43
rob mayoff

@robmayoffに感謝します。より一般的なデリゲートインターセプターになるようにカプセル化しました。元のMessageInterceptorクラスがあります。

MessageInterceptor.h

@interface MessageInterceptor : NSObject

@property (nonatomic, assign) id receiver;
@property (nonatomic, assign) id middleMan;

@end

MessageInterceptor.m

@implementation MessageInterceptor

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if ([self.middleMan respondsToSelector:aSelector]) { return    self.middleMan; }
    if ([self.receiver respondsToSelector:aSelector]) { return self.receiver; }
    return [super forwardingTargetForSelector:aSelector];
}

- (BOOL)respondsToSelector:(SEL)aSelector {
    if ([self.middleMan respondsToSelector:aSelector]) { return YES; }
    if ([self.receiver respondsToSelector:aSelector]) { return YES; }
    return [super respondsToSelector:aSelector];
}

@end

これは、汎用デリゲートクラスで使用されます。

GenericDelegate.h

@interface GenericDelegate : NSObject

@property (nonatomic, strong) MessageInterceptor * delegate_interceptor;

- (id)initWithDelegate:(id)delegate;

@end

GenericDelegate.m

@implementation GenericDelegate

- (id)initWithDelegate:(id)delegate {
    self = [super init];
    if (self) {
        self.delegate_interceptor = [[MessageInterceptor alloc] init];
        [self.delegate_interceptor setMiddleMan:self];
        [self.delegate_interceptor setReceiver:delegate];
    }
    return self;
}

// delegate methods I wanna override:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    // 1. your custom code goes here
    NSLog(@"Intercepting scrollViewDidScroll: %f %f", scrollView.contentOffset.x, scrollView.contentOffset.y);

    // 2. forward to the delegate as usual
    if ([self.delegate_interceptor.receiver respondsToSelector:@selector(scrollViewDidScroll:)]) {
        [self.delegate_interceptor.receiver scrollViewDidScroll:scrollView];
    }
 }
//other delegate functions you want to intercept
...

したがって、UITableView、UICollectionView、UIScrollView ...で必要なデリゲートをインターセプトできます。

@property (strong, nonatomic) GenericDelegate *genericDelegate;
@property (nonatomic, strong) UICollectionView* collectionView;

//intercepting delegate in order to add separator line functionality on this scrollView
self.genericDelegate = [[GenericDelegate alloc]initWithDelegate:self];
self.collectionView.delegate = (id)self.genericDelegate.delegate_interceptor;

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    NSLog(@"Original scrollViewDidScroll: %f %f", scrollView.contentOffset.x, scrollView.contentOffset.y);
}

この場合、UICollectionViewDelegate scrollViewDidScroll:関数は、GenericDelegate(追加するコードを含む)および独自のクラスの実装で実行されます。

@robmayoffと@jhabbottの以前の回答のおかげで、それは私の5セントです

2
M Penades

別のオプションは、抽象関数の力をサブクラス化して使用することです。たとえば、あなたは

@interface EnhancedTableViewController : UITableViewController <UITableViewDelegate>

そこで、いくつかのデリゲートメソッドをオーバーライドし、抽象関数を定義します

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    // code before interception
    [self bfTableView:(UITableView *)tableView didSelectRowAtIndexPath:indexPath];
    // code after interception
}

- (void)bfTableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {};

これで、EnhancedTableViewControllerをサブクラス化し、デリゲート関数の代わりに抽象関数を使用するだけです。このような:

@interface MyTableViewController : EnhancedTableViewController ...

- (void)bfTableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    //overriden implementation with pre/post actions
};

ここに何か問題があれば教えてください。

1
Roman B.

UIControlクラスでこの種のことをしたいとお考えの方は、私の STAControlsプロジェクト (具体的にはBaseクラス このような を参照してください。 )。 Rob Mayoffの回答 を使用して、プロジェクトのデリゲート転送を実装しました。

0
Stunner

サブクラス化せず、代わりにカプセル化します;)

新しいUIViewサブクラスを作成します-ヘッダーファイルは次のようになります:

@interface MySubclass : UIView 
@end

そして、サブビューとしてUIScrollViewを使用するだけです。mファイルは次のようになります。

@interface MySubclass () <UIScrollViewDelegate>

@property (nonatomic, strong, readonly) UIScrollView *scrollView;

@end


@implementation MySubClass

@synthesize scrollView = _scrollView;

- (UIScrollView *)scrollView {
    if (nil == _scrollView) {
        _scrollView = [UIScrollView alloc] initWithFrame:self.bounds];
        _scrollView.delegate = self;
        [self addSubView:_scrollView];
    }
    return _scrollView;
}

...
your code here
...

@end

サブクラス内では、常にself.scrollView初めてスクロールビューを要求したときに、スクロールビューが作成されます。

これには、MySubClassを使用している人からスクロールビューを完全に隠すという利点があります。バックグラウンドでの動作を変更する必要がある場合(つまり、スクロールビューからWebビューに変更する場合)は、非常に簡単です。行う :)

また、スクロールビューの動作を誰も変更できないことも意味します:)

PS私はARCを想定しています-strongretainに変更し、必要に応じてdeallocを追加します:)


編集

クラスをexactlyとしてUIScrollViewとして動作させたい場合は、このメソッドを追加してみてください( the docs から取得)。テストされていません!):

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    if ([self.scrollView respondsToSelector:anInvocation.selector])
        [anInvocation invokeWithTarget:self.scrollView];
    else
        [super forwardInvocation:anInvocation];
}
0
deanWombourne