objective-c ios delegates uiscrollview subclass

objective c - Cómo subclasificar UIScrollView y hacer que la propiedad del delegado sea privada



objective-c ios (5)

Gracias @robmayoff Encapsulado para ser un interceptor delegado más genérico: Tener la clase original 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

Se utiliza en su clase de delegado genérico:

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

Así que puedo interceptar cualquier delegado que necesite en cualquier 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); }

En este caso, la función UICollectionViewDelegate scrollViewDidScroll: se ejecutará en nuestro GenericDelegate (con cualquier código que queramos agregar) y en la implementación de nuestra propia clase

Esos son mis 5 centavos, gracias a la respuesta anterior de @robmayoff y @jhabbott

Esto es lo que quiero lograr:

Quiero subclasificar un UIScrollView para tener una funcionalidad adicional. Esta subclase debería poder reaccionar en el desplazamiento, por lo que tengo que configurar la propiedad del delegado en auto para recibir eventos como:

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

Por otro lado, otras clases también deberían poder recibir estos eventos, como si estuvieran usando la clase base UIScrollView.

Así que tuve diferentes ideas sobre cómo resolver ese problema, pero todas estas no me satisfacen por completo :(

Mi enfoque principal es ... usar una propiedad de delegado propia como esta:

@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

De esa manera, mi subclase puede hacer algo especial cuando la vista de desplazamiento heredada finaliza el desplazamiento, pero aún así informa al delegado externo del evento. Eso funciona hasta ahora. Pero como quiero que esta subclase esté disponible para otros desarrolladores, quiero restringir el acceso a la propiedad del delegado de la clase base, ya que solo debería ser utilizada por la subclase. Creo que es muy probable que otros desarrolladores utilicen intuitivamente la propiedad delegada de la clase base, incluso si comento el problema en el archivo de encabezado. Si alguien altera la propiedad del delegado, la subclase no hará lo que se supone que debe hacer y no puedo hacer nada para evitarlo ahora mismo. Y ese es el punto en el que no tengo ni idea de cómo resolverlo.

Lo que intenté es intentar anular la propiedad del delegado para que sea tan solo así:

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

Eso resultará en una advertencia

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

Ok mala idea, ya que obviamente estoy violando el principio de sustitución de liskov aquí.

Siguiente intento -> Intentando anular el establecedor de delegado de esta manera:

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

Como se comentó, este ejemplo no se compila, ya que parece que no se encontró el _delegate ivar. Así que busqué el archivo de cabecera de UIScrollView y encontré esto:

@package ... id _delegate; ...

La directiva @package restringe el acceso de _delegate ivar a ser accesible solo por el propio framework. Entonces, cuando quiero configurar _delegate ivar TENGO QUE usar el configurador sintetizado. No puedo ver una manera de anularlo de ninguna manera :( Pero no puedo creer que no haya una forma de evitar esto, tal vez no pueda ver la madera de los árboles.

Agradezco cualquier sugerencia para resolver este problema.

Solución:

Funciona ahora con la solución de @rob mayoff. Como comenté justo debajo, hubo un problema con la llamada scrollViewDidScroll :. Finalmente descubrí cuál es el problema, aunque no entiendo por qué esto es así: /

Justo en el momento en que configuramos el super delegado:

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

hay una devolución de llamada a _myDelegate. El depurador se rompe en

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

con el selector "scrollViewDidScroll:" como argumento.

Lo gracioso en este momento self.userDelegate aún no está configurado y apunta a cero, por lo que el valor de retorno es NO! Eso parece hacer que los métodos scrollViewDidScroll: no se activen después. Parece un chequeo previo si se implementa el método y si falla, este método no se activará en absoluto, incluso si configuramos nuestra propiedad UserDelegate más adelante. No sé por qué esto es así, ya que la mayoría de los otros métodos de delegado no tienen esta comprobación previa.

Así que mi solución para esto es invocar el método [super setDelegate ...] en el método PrivateDelegate setDelegate, ya que este es el lugar donde estoy bastante seguro de que mi método UserDelegate está configurado.

Así que voy a terminar con este fragmento de implementación:

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

El resto del código permanece intacto. Todavía no estoy realmente satisfecho con esta solución, porque hace necesario llamar al método setDelegate al menos una vez, pero por el momento funciona para mis necesidades, aunque se siente muy intrépido: /

Si alguien tiene ideas de cómo mejorar eso, lo apreciaría.

Gracias @rob por tu ejemplo!


Hay un problema con hacer de MySubclass su propio delegado. Es de suponer que no desea ejecutar código personalizado para todos los métodos UIScrollViewDelegate , pero tiene que reenviar los mensajes al delegado proporcionado por el usuario, ya sea que tenga su propia implementación o no. Así que podrías intentar implementar todos los métodos de delegado, con la mayoría de ellos simplemente reenviando así:

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

El problema aquí es que a veces las nuevas versiones de iOS agregan nuevos métodos de delegado. Por ejemplo, iOS 5.0 agregó scrollViewWillEndDragging:withVelocity:targetContentOffset: Por lo tanto, su subclase scrollview no estará preparada para el futuro.

La mejor manera de manejar esto es crear un objeto privado separado que solo actúe como delegado de su scrollview y maneje el reenvío. Este objeto de delegado dedicado puede reenviar todos los mensajes que recibe al delegado proporcionado por el usuario, porque solo recibe mensajes de delegado.

Esto es lo que haces. En su archivo de encabezado, solo necesita declarar la interfaz para su subclase scrollview. No es necesario que expongas ningún método o propiedad nuevos, por lo que se ve así:

MyScrollView.h

@interface MyScrollView : UIScrollView @end

Todo el trabajo real se realiza en el archivo .m . Primero, definimos la interfaz para la clase de delegado privado. Su trabajo es volver a llamar a MyScrollView para algunos de los métodos de delegado y reenviar todos los mensajes al delegado del usuario. Así que solo queremos darle métodos que sean parte de UIScrollViewDelegate . No queremos que tenga métodos adicionales para administrar una referencia al delegado del usuario, así que mantendremos esa referencia como una variable de instancia:

MyScrollView.m

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

A continuación vamos a implementar MyScrollView . Necesita crear una instancia de MyScrollViewPrivateDelegate , que debe poseer. Dado que un UIScrollView no posee a su delegado, necesitamos una referencia fuerte y adicional a este objeto.

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

Necesitamos anular setDelegate: y delegate: para almacenar y devolver una referencia al delegado del usuario:

- (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; }

También necesitamos definir cualquier método adicional que nuestro delegado privado deba usar:

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

Ahora podemos definir finalmente la implementación de MyScrollViewPrivateDelegate . Necesitamos definir explícitamente cada método que debe contener nuestro código personalizado privado. El método debe ejecutar nuestro código personalizado y reenviar el mensaje al delegado del usuario, si el delegado del usuario responde al mensaje:

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

Y debemos manejar todos los demás métodos de UIScrollViewDelegate para los cuales no tenemos un código personalizado, y todos esos mensajes que se agregarán en futuras versiones de iOS. Tenemos que implementar dos métodos para que eso suceda:

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


No lo subclasifique, encapsúlelo en su lugar;)

Cree una nueva subclase UIView: su archivo de encabezado se vería así:

@interface MySubclass : UIView @end

Y solo tenga un UIScrollView como una subvista: su archivo .m se vería así:

@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

Dentro de su subclase, use siempre self.scrollView y creará la vista de desplazamiento la primera vez que la solicite.

Esto tiene la ventaja de ocultar la vista de desplazamiento por completo a cualquiera que use su MySubClass . Si tuviera que cambiar la forma en que funcionaba entre bastidores (es decir, el cambio de una vista de desplazamiento a una vista web) sería muy fácil de hacer :)

También significa que nadie puede alterar cómo desea que se comporte la vista de desplazamiento :)

PD: asumí ARC: cambia strong para retain y agregar dealloc si es necesario :)

EDITAR

Si desea que su clase se comporte exactamente como un UIScrollView, entonces puede intentar esto agregando este método (¡tomado de estos documentos y sin probar!):

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


Otra opción es crear subclases y usar el poder de las funciones abstractas. Por ejemplo, creas

@interface EnhancedTableViewController : UITableViewController <UITableViewDelegate>

Allí anula algún método de delegado y define su función abstracta

- (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 {};

Ahora todo lo que necesita hacer es crear una subclase de su EnhancedTableViewController y usar sus funciones abstractas en lugar de delegar. Me gusta esto:

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

Déjame saber si hay algo malo aquí.


Para aquellos de ustedes que buscan hacer este tipo de cosas con UIControl clases de UIControl , vean mi proyecto STAControls (específicamente clases Base como esta ). He implementado el reenvío de delegado para el proyecto utilizando la respuesta de Rob Mayoff .