Вы когда-нибудь писали адаптеры для Keychain или NSUserDefaults? Они полностью состоят из однотипных сеттеров и геттеров. Предлагаю написать логику один раз, предоставив остальное рантайму. За реализацией прошу под кат.
Привет. С вами вновь vdugnist из FunCorp. Недавно, при добавлении нового поля в адаптер Keychain, я поймал ошибку при копировании кода из соседнего метода.
Как выглядела реализация до этого:
- (Credentials *)credentials {
// implementation details
}
- (void)setCredentials:(Credentials *)credentials {
// implementation details
}
- (NSDate *)firstLaunchDate {
// implementation details
}
- (void)setFirstLaunchDate:(NSDate *)date {
// implementation details
}
Код попросту копировался из ближайшего метода и в нём менялись константы. Понятно, что при таком подходе можно легко допустить ошибку. После небольшого рефакторинга получилось вынести всю реализацию логики класса в два метода:
- (Credentials *)credentials {
return [self objectFromKeychainForKey:@"credentials"];
}
- (void)setCredentials:(Credentials *)credentials {
[self setObject:credentials toKeychainForKey:@"credentials"];
}
- (NSDate *)firstLaunchDate {
return [self objectFromKeychainForKey:@"firstLaunchDate"];
}
- (void)setFirstLaunchDate:(NSDate *)firstLaunchDate {
[self setObject:firstLaunchDate toKeychainForKey:@"firstLaunchDate"];
}
- (void)setObject:(id)obj toKeychainForKey:(NSString *)key {
// implementation details
}
- (id)objectFromKeychainForKey:(NSString *)key {
// implementation details
}
Уже лучше. Но остаётся ещё две проблемы:
- всё ещё можно опечататься в строковой константе, которую мы передаём в метод;
- весь класс будет состоять из методов с одинаковой реализацией, отличающихся только аргументом у вызываемого метода.
И тут нам на помощь приходит рантайм. В Objective-C при добавлении @property в интерфейс класса автоматически генерируется сеттер, геттер и ivar. В стандартной реализации сеттера значение записывается в ivar, а для геттера — читается из ivar. Для того чтобы эти методы не генерировались, в реализации класса вам нужно написать dynamic <имя поля>. Тогда при обращению к полю мы получим исключение unrecognized selector sent to instance.
Перед отправкой исключения у класса будет вызван метод +(BOOL)resolveInstanceMethod:(SEL)sel
в случае instance property или +(BOOL)resolveClassMethod:(SEL)sel
в случае class property.
В них можно добавить реализацию метода по селектору с помощью class_addMethod
и вернуть YES
, если всё прошло гладко. После этого для текущего и последующих вызовов будет вызвана реализация вновь добавленного метода.
Для добавления метода в рантайме вам понадобится указатель на класс, к которому будет добавлен метод, селектор, блок с реализацией и сигнатура метода. Документацию по последнему аргументу можно почитать вот тут.
Я сразу решил выносить решение моей проблемы в библиотеку, поэтому в примере обработаны и class property, и instance property. В примере используются вспомогательные функции, реализацию можно посмотреть тут.
+ (BOOL)resolveClassMethod:(SEL)sel {
return [self resolveMethodFor:object_getClass(self) selector:sel];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return [self resolveMethodFor:self selector:sel];
}
+ (BOOL)resolveMethodFor:(id)target selector:(SEL)sel {
if (!sel_isGetterOrSetter(target, sel)) {
return NO;
}
objc_property_t property = propertyForSelector(target, sel);
if (sel_isSetter(target, sel)) {
SEL getterSel = sel_getterFromSetter(sel);
dvPropertySetterBlock setterBlock = [self setterBlockForTarget:target getterSelector:getterSel];
IMP blockImplementation = imp_implementationWithBlock(setterBlock);
char *methodTypes = copySetterMethodTypesForProperty(property);
assert(class_addMethod(target, sel, blockImplementation, methodTypes));
free(methodTypes);
}
else {
dvPropertyGetterBlock getterBlock = [self getterBlockForTarget:target getterSelector:sel];
IMP blockImplementation = imp_implementationWithBlock(getterBlock);
char *methodTypes = copyGetterMethodTypesForProperty(property);
assert(class_addMethod(target, sel, blockImplementation, methodTypes));
free(methodTypes);
}
return YES;
}
+ (dvPropertySetterBlock)setterBlockForTarget:(id)target getterSelector:(SEL)getterSelector {
@throw @"Override this method in subclass";
}
+ (dvPropertyGetterBlock)getterBlockForTarget:(id)target getterSelector:(SEL)getterSelector {
@throw @"Override this method in subclass";
}
В наследниках достаточно переопределить два метода (блок геттера и блок сеттера), добавить @property в интерфейс и dynamic в реализацию. Вот, например, реализация адаптера к NSUserDefaults:
+ (dvPropertySetterBlock)setterBlockForTarget:(id)target getterSelector:(SEL)getterSelector {
return ^(id blockSelf, id value) {
[[NSUserDefaults standardUserDefaults] setObject:value forKey:NSStringFromSelector(getterSelector)];
};
}
+ (dvPropertyGetterBlock)getterBlockForTarget:(id)target getterSelector:(SEL)getterSelector {
return ^id(id blockSelf) {
return [[NSUserDefaults standardUserDefaults] objectForKey:NSStringFromSelector(getterSelector)];
};
}
Саму библиотеку можно найти на гитхабе, а я готов ответить на ваши вопросы в комментариях.
Автор: vdugnist