Suprima la advertencia "La categoría está implementando un método que también será implementado por su clase primaria"

98

Me preguntaba cómo suprimir la advertencia:

Category está implementando un método que también será implementado por su clase primaria.

Tengo esto para una categoría de código específica:

+ (UIFont *)systemFontOfSize:(CGFloat)fontSize {
    return [self aCustomFontOfSize:fontSize];
}
Doz
fuente
Por método swizzling. Aunque yo no haría eso, tal vez podría crear una subclase UIFont que anule el mismo método y llamar de superotra manera.
Alan Zeino
4
Tu problema no es la advertencia. Su problema es que tiene el mismo nombre de método, lo que generará problemas.
gnasher729
Consulte Anular métodos que utilizan categorías en Objective-C para conocer las razones por las que no debería anular métodos que utilizan categorías y soluciones alternativas.
Sensible
Si ustedes conocen una solución más elegante para configurar la fuente para toda la aplicación, ¡realmente me gustaría escucharla!
Hasta el

Respuestas:

64

Una categoría le permite agregar nuevos métodos a una clase existente. Si desea volver a implementar un método que ya existe en la clase, normalmente crea una subclase en lugar de una categoría.

Documentación de Apple: personalización de clases existentes

Si el nombre de un método declarado en una categoría es el mismo que un método en la clase original, o un método en otra categoría en la misma clase (o incluso una superclase), el comportamiento no está definido en cuanto a qué implementación del método se usa en tiempo de ejecución.

Dos métodos con exactamente la misma firma en la misma clase darían lugar a un comportamiento impredecible, porque cada llamador no puede especificar qué implementación desea.

Por lo tanto, debe usar una categoría y proporcionar nombres de método que sean nuevos y únicos para la clase o subclase si desea cambiar el comportamiento de un método existente en una clase.

bneely
fuente
1
Estoy totalmente de acuerdo con las ideas explicadas anteriormente y trato de seguirlas durante el desarrollo. pero aún puede haber casos en los que los métodos de anulación en la categoría podrían ser apropiados. por ejemplo, casos en los que se podría utilizar herencia múltiple (como en c ++) o interfaces (como en c #). acabo de enfrentarme a eso en mi proyecto y me di cuenta de que los métodos primarios en categorías son la mejor opción.
peetonn
4
Puede ser útil cuando se realiza una prueba unitaria de un código que tiene un singleton. Idealmente, los singleton deben inyectarse en el código como un protocolo, lo que le permite cambiar la implementación. Pero si ya tiene uno incrustado dentro de su código, puede agregar una categoría del singleton en su prueba unitaria y anular la SharedInstance y los métodos que debe controlar para convertirlos en objetos ficticios.
bandejapaisa
Gracias @PsychoDad. Actualicé el enlace y agregué una cita de la documentación que es relevante para esta publicación.
bneely
Se ve bien. ¿Apple proporciona documentación sobre el comportamiento de usar una categoría con un nombre de método existente?
jjxtra
1
impresionante, no estaba seguro de si debería ir con categoría o subclase :-)
kernix
343

Aunque todo lo que se ha dicho correctamente es correcto, en realidad no responde a su pregunta de cómo suprimir la advertencia.

Si tiene que tener este código por alguna razón (en mi caso, tengo HockeyKit en mi proyecto y anulan un método en una categoría UIImage [editar: este ya no es el caso]) y necesita que su proyecto se compile , puede usar #pragmadeclaraciones para bloquear la advertencia de la siguiente manera:

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation"

// do your override

#pragma clang diagnostic pop

Encontré la información aquí: http://www.cocoabuilder.com/archive/xcode/313767-disable-warning-for-override-in-category.html

Ben Baron
fuente
¡Muchas gracias! Entonces, veo que los pragmas también pueden suprimir las advertencias. :-p
Constantino Tsarouhas
Sí, y aunque estas son declaraciones específicas de LLVM, también hay declaraciones similares para GCC.
Ben Baron
1
La advertencia en su proyecto de prueba es una advertencia del enlazador, no una advertencia del compilador de llvm, por lo tanto, el pragma de llvm no está haciendo nada. Sin embargo, notará que su proyecto de prueba aún se compila con "tratar las advertencias como errores" activado porque es una advertencia del vinculador.
Ben Baron
12
Esta debería ser la respuesta aceptada, dado que realmente responde a la pregunta.
Rob Jones
1
Esta respuesta debería ser la correcta. De todos modos tiene más votos que el seleccionado como respuesta.
Juan Catalan
20

Una mejor alternativa (vea la respuesta de bneely a por qué esta advertencia lo está salvando de un desastre) es usar el método swizzling. Al utilizar la combinación de métodos, puede reemplazar un método existente de una categoría sin la incertidumbre de quién "gana", y al mismo tiempo conservando la capacidad de llamar al método anterior. El secreto es darle a la anulación un nombre de método diferente y luego intercambiarlos usando funciones de tiempo de ejecución.

#import <objc/runtime.h> 
#import <objc/message.h>

void MethodSwizzle(Class c, SEL orig, SEL new) {
    Method origMethod = class_getInstanceMethod(c, orig);
    Method newMethod = class_getInstanceMethod(c, new);
    if(class_addMethod(c, orig, method_getImplementation(newMethod), method_getTypeEncoding(newMethod)))
        class_replaceMethod(c, new, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));
    else
    method_exchangeImplementations(origMethod, newMethod);
}

Luego defina su implementación personalizada:

+ (UIFont *)mySystemFontOfSize:(CGFloat)fontSize {
...
}

Anula la implementación predeterminada con la tuya:

MethodSwizzle([UIFont class], @selector(systemFontOfSize:), @selector(mySystemFontOfSize:));
Sanjit Saluja
fuente
10

Prueba esto en tu código:

+(void)load{
    EXCHANGE_METHOD(Method1, Method1Impl);
}

ACTUALIZACIÓN2: agregue esta macro

#import <Foundation/Foundation.h>
#define EXCHANGE_METHOD(a,b) [[self class]exchangeMethod:@selector(a) withNewMethod:@selector(b)]

@interface NSObject (MethodExchange)
+(void)exchangeMethod:(SEL)origSel withNewMethod:(SEL)newSel;
@end

#import <objc/runtime.h>

@implementation NSObject (MethodExchange)

+(void)exchangeMethod:(SEL)origSel withNewMethod:(SEL)newSel{
    Class class = [self class];

    Method origMethod = class_getInstanceMethod(class, origSel);
    if (!origMethod){
        origMethod = class_getClassMethod(class, origSel);
    }
    if (!origMethod)
        @throw [NSException exceptionWithName:@"Original method not found" reason:nil userInfo:nil];
    Method newMethod = class_getInstanceMethod(class, newSel);
    if (!newMethod){
        newMethod = class_getClassMethod(class, newSel);
    }
    if (!newMethod)
        @throw [NSException exceptionWithName:@"New method not found" reason:nil userInfo:nil];
    if (origMethod==newMethod)
        @throw [NSException exceptionWithName:@"Methods are the same" reason:nil userInfo:nil];
    method_exchangeImplementations(origMethod, newMethod);
}

@end
Vitaliy Gervazuk
fuente
1
Este no es un ejemplo completo. Ninguna macro denominada EXCHANGE_METHOD está realmente definida por el tiempo de ejecución de objetivo-c.
Richard J. Ross III
@Vitaly stil -1. ese método no está implementado para el tipo de clase. ¿Qué marco estás usando?
Richard J. Ross III
Lo siento de nuevo, pruebe esto. Creé el archivo NSObject + MethodExchange
Vitaliy Gervazuk
Con la categoría en NSObject, ¿por qué molestarse con la macro? ¿Por qué no simplemente usar el 'método de intercambio'?
hvanbrug
5

Puede utilizar el método swizzling para suprimir esta advertencia del compilador. Así es como implementé el método swizzling para dibujar márgenes en un UITextField cuando usamos un fondo personalizado con UITextBorderStyleNone:

#import <UIKit/UIKit.h>

@interface UITextField (UITextFieldCatagory)

+(void)load;
- (CGRect)textRectForBoundsCustom:(CGRect)bounds;
- (CGRect)editingRectForBoundsCustom:(CGRect)bounds;
@end

#import "UITextField+UITextFieldCatagory.h"
#import <objc/objc-runtime.h>

@implementation UITextField (UITextFieldCatagory)

+(void)load
{
    Method textRectForBounds = class_getInstanceMethod(self, @selector(textRectForBounds:));
    Method textRectForBoundsCustom = class_getInstanceMethod(self, @selector(textRectForBoundsCustom:));

    Method editingRectForBounds = class_getInstanceMethod(self, @selector(editingRectForBounds:));
    Method editingRectForBoundsCustom = class_getInstanceMethod(self, @selector(editingRectForBoundsCustom:));


    method_exchangeImplementations(textRectForBounds, textRectForBoundsCustom);
    method_exchangeImplementations(editingRectForBounds, editingRectForBoundsCustom);

}


- (CGRect)textRectForBoundsCustom:(CGRect)bounds
{
    CGRect inset = CGRectMake(bounds.origin.x + 10, bounds.origin.y, bounds.size.width - 10, bounds.size.height);
    return inset;
}

- (CGRect)editingRectForBoundsCustom:(CGRect)bounds
{
    CGRect inset = CGRectMake(bounds.origin.x + 10, bounds.origin.y, bounds.size.width - 10, bounds.size.height);
    return inset;
}

@end
Say2Manuj
fuente
2

Las propiedades primarias son válidas para una extensión de clase (categoría anónima), pero no para una categoría normal.

Según Apple Docs, utilizando una extensión de clase (categoría anónima), puede crear una interfaz privada para una clase pública, de modo que la interfaz privada pueda anular las propiedades expuestas públicamente. es decir, puede cambiar una propiedad de readonly a readwrite.

Un caso de uso para esto es cuando escribe bibliotecas que restringen el acceso a propiedades públicas, mientras que la misma propiedad necesita acceso completo de lectura y escritura dentro de la biblioteca.

Enlace de Apple Docs: https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/CustomizingExistingClasses/CustomizingExistingClasses.html

Busque " Usar extensiones de clase para ocultar información privada ".

Entonces, esta técnica es válida para una Extensión de clase, pero no para una Categoría.

Kris Subramanian
fuente
1

Las categorías son algo bueno, pero se puede abusar de ellas. Al escribir categorías, como principio, NO debe volver a implementar los métodos existentes. Si lo hace, puede causar un efecto secundario extraño, ya que ahora está reescribiendo el código de que otra clase depende de una. podría romper una clase conocida y terminar volviendo su depurador al revés. Es simplemente una mala programación.

Si necesita hacerlo, debería subclasificarlo.

Entonces la sugerencia de swizzling, eso es un gran NO-NO-NO para mí.

Pasarlo en tiempo de ejecución es un NO-NO-NO completo.

¿Quieres que un plátano se vea como una naranja, pero solo en tiempo de ejecución? Si quieres una naranja, escribe una naranja.

No hagas que un plátano se vea y actúes como una naranja. Y lo que es peor: no conviertas tu plátano en un agente secreto que saboteará silenciosamente los plátanos en todo el mundo en apoyo de las naranjas.

¡Ay!

Leander
fuente
3
Sin embargo, usar swizzing en tiempo de ejecución puede ser útil para burlarse de comportamientos en el entorno de prueba.
Ben G
2
Aunque es graciosa, su respuesta no hace más que decir que todos los métodos posibles son malos. La naturaleza de la bestia es que a veces realmente no puedes hacer una subclase, por lo que te quedas con una categoría y, especialmente si no tienes el código de la clase que estás categorizando, a veces necesitas hacer el swizzle, y ser un método indeseable no es relevante.
hvanbrug
1

Tuve este problema cuando implementé un método delegado en una categoría en lugar de la clase principal (aunque no había una implementación de clase principal). La solución para mí fue mover del archivo de encabezado de clase principal al archivo de encabezado de categoría Esto funciona bien

gheese
fuente