Suprimir o aviso “A categoria está implementando um método que também será implementado por sua classe primária”

98

Eu queria saber como suprimir o aviso:

Categoria está implementando um método que também será implementado por sua classe primária.

Eu tenho isso para uma categoria de código específica:

+ (UIFont *)systemFontOfSize:(CGFloat)fontSize {
    return [self aCustomFontOfSize:fontSize];
}
Doz
fonte
Pelo método swizzling. Embora eu não fizesse isso - talvez você pudesse criar uma subclasse UIFont que substitui o mesmo método e chamar de superoutra forma.
Alan Zeino
4
Seu problema não é o aviso. O seu problema é que você tem o mesmo nome de método, o que pode causar problemas.
gnasher729
Consulte Substituindo métodos usando categorias em Objective-C para saber por que você não deve substituir métodos usando categorias e para soluções alternativas.
Senseful
Se vocês conhecem uma solução mais elegante para definir a fonte em todo o aplicativo, eu realmente gostaria de ouvi-la!
To1ne,

Respostas:

64

Uma categoria permite adicionar novos métodos a uma classe existente. Se você deseja reimplementar um método que já existe na classe, normalmente cria uma subclasse em vez de uma categoria.

Documentação da Apple: Customizando classes existentes

Se o nome de um método declarado em uma categoria for o mesmo que um método na classe original, ou um método em outra categoria na mesma classe (ou mesmo uma superclasse), o comportamento é indefinido quanto a qual implementação de método é usada em tempo de execução.

Dois métodos com a mesma assinatura exata na mesma classe levariam a um comportamento imprevisível, porque cada chamador não pode especificar qual implementação deseja.

Portanto, você deve usar uma categoria e fornecer nomes de método que sejam novos e exclusivos para a classe, ou subclasse, se desejar alterar o comportamento de um método existente em uma classe.

benevolamente
fonte
1
Eu concordo totalmente com as idéias explicadas acima e tento segui-las durante o desenvolvimento. mas ainda pode haver casos em que métodos de substituição na categoria podem ser apropriados. por exemplo, casos em que herança múltipla (como em c ++) ou interfaces (como em c #) podem ser usadas. acabei de enfrentar isso em meu projeto e percebi que métodos de substituição em categorias são a melhor escolha.
peetonn
4
Pode ser útil ao testar a unidade de algum código que contém um singleton. Idealmente, singletons devem ser injetados no código como um protocolo, permitindo que você alterne a implementação. Mas se você já tem um embutido em seu código, pode adicionar uma categoria do singleton em seu teste de unidade e substituir o sharedInstance e os métodos que você deve controlar para transformá-los em objetos fictícios.
bandejapaisa
Obrigado @PsychoDad. Eu atualizei o link e adicionei uma citação da documentação que é relevante para este post.
Bneely
Parece bom. A Apple fornece documentação sobre o comportamento de usar uma categoria com um nome de método existente?
jjxtra
1
incrível, não tinha certeza se deveria
escolher
343

Embora tudo o que foi dito corretamente esteja correto, na verdade não responde à sua pergunta sobre como suprimir o aviso.

Se você precisar inserir esse código por algum motivo (no meu caso, tenho HockeyKit em meu projeto e eles substituem um método em uma categoria UIImage [editar: este não é mais o caso]) e você precisa fazer com que seu projeto seja compilado , você pode usar #pragmainstruções para bloquear o aviso, como:

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

// do your override

#pragma clang diagnostic pop

Encontrei as informações aqui: http://www.cocoabuilder.com/archive/xcode/313767-disable-warning-for-override-in-category.html

Ben Baron
fonte
Muito obrigado! Portanto, vejo que os pragmas também podem suprimir os avisos. :-p
Constantino Tsarouhas
Sim, e embora essas sejam instruções específicas do LLVM, há outras semelhantes para o GCC também.
Ben Baron
1
O aviso em seu projeto de teste é um aviso do vinculador, não um aviso do compilador llvm, portanto, o pragma llvm não está fazendo nada. No entanto, você notará que seu projeto de teste ainda é compilado com "tratar avisos como erros" ativado porque é um aviso do vinculador.
Ben Baron,
12
Essa realmente deveria ser a resposta aceita, visto que realmente responde à pergunta.
Rob Jones,
1
Essa resposta deve ser a correta. De qualquer forma tem mais votos do que o selecionado como resposta.
Juan Catalan
20

Uma alternativa melhor (veja a resposta de Bneely sobre por que esse aviso está salvando você de um desastre) é usar o método swizzling. Ao usar o método swizzling, você pode substituir um método existente de uma categoria sem a incerteza de quem "vence" e ao mesmo tempo preservando a capacidade de chamar o método antigo. O segredo é dar à substituição um nome de método diferente e trocá-los usando funções de tempo de execução.

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

Em seguida, defina sua implementação personalizada:

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

Substitua a implementação padrão pela sua:

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

Experimente isso em seu código:

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

ATUALIZAÇÃO2: Adicionar 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
fonte
1
Este não é um exemplo completo. Nenhuma macro chamada EXCHANGE_METHOD é realmente definida pelo tempo de execução objetivo-c.
Richard J. Ross III
@Vitaly stil -1. esse método não é implementado para o tipo de classe. Qual estrutura você está usando?
Richard J. Ross III
Desculpe novamente, tente fazer isso eu criei o arquivo NSObject + MethodExchange
Vitaliy Gervazuk
Com a categoria no NSObject, por que se preocupar com a macro? Por que não apenas swizzle com o 'exchangeMethod'?
hvanbrug
5

Você pode usar o método swizzling para suprimir este aviso do compilador. Aqui está como eu implementei o método swizzling para desenhar margens em um UITextField quando usamos um fundo personalizado com 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
fonte
2

Propriedades de substituição são válidas para uma extensão de classe (categoria anônima), mas não para uma categoria regular.

De acordo com o Apple Docs, usando uma extensão de classe (categoria anônima), você pode criar uma interface privada para uma classe pública, de forma que a interface privada possa substituir as propriedades expostas publicamente. ou seja, você pode alterar uma propriedade de somente leitura para leitura e gravação.

Um caso de uso para isso é quando você grava bibliotecas que restringem o acesso a propriedades públicas, enquanto a mesma propriedade precisa de acesso total de leitura e gravação dentro da biblioteca.

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

Pesquise " Use extensões de classe para ocultar informações privadas ".

Portanto, esta técnica é válida para uma extensão de classe, mas não para uma categoria.

Kris Subramanian
fonte
1

As categorias são boas, mas podem ser abusadas. Ao escrever categorias, você deve, por princípio, NÃO reimplementar os métodos existentes. Fazer isso pode causar um efeito colateral estranho, já que agora você está reescrevendo código do qual outra classe depende. você pode quebrar uma classe conhecida e acabar virando seu depurador do avesso. É simplesmente uma programação ruim.

Se você precisa fazer isso, você realmente deve subclassificá-lo.

Então a sugestão de swizzling, que é um grande NÃO-NÃO-NÃO para mim.

Swizzing em tempo de execução é um NO-NO-NO completo.

Você quer que uma banana se pareça com uma laranja, mas apenas em tempo de execução? Se você quiser uma laranja, escreva uma laranja.

Não faça uma banana parecer e agir como uma laranja. E pior: não transforme sua banana em um agente secreto que sabotará bananas em todo o mundo em apoio às laranjas.

Caramba!

Leander
fonte
3
Swizzing em tempo de execução pode ser útil para simular comportamentos em ambiente de teste, no entanto.
Ben G
2
Embora engraçada, sua resposta não faz mais do que dizer que todos os métodos possíveis são ruins. A natureza da besta é que às vezes você realmente não pode criar uma subclasse, então você fica com uma categoria e especialmente se você não possui o código da classe que está categorizando, às vezes você precisa fazer o swizzle, e ser um método indesejável não é relevante.
hvanbrug
1

Tive esse problema quando implementei um método delegado em uma categoria em vez da classe principal (embora não houvesse implementação da classe principal). A solução para mim foi mover do arquivo de cabeçalho de classe principal para o arquivo de cabeçalho de categoria. Isso funciona bem

gheese
fonte