Хабрахабр

Однотипные методы? Хватит это писать

Они полностью состоят из однотипных сеттеров и геттеров. Вы когда-нибудь писали адаптеры для Keychain или NSUserDefaults? За реализацией прошу под кат. Предлагаю написать логику один раз, предоставив остальное рантайму.

клавиатура с кнопками copy и paste

С вами вновь 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. И тут нам на помощь приходит рантайм. Для того чтобы эти методы не генерировались, в реализации класса вам нужно написать dynamic <имя поля>. В стандартной реализации сеттера значение записывается в ivar, а для геттера — читается из ivar. Тогда при обращению к полю мы получим исключение 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 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";
}

Вот, например, реализация адаптера к NSUserDefaults: В наследниках достаточно переопределить два метода (блок геттера и блок сеттера), добавить @property в интерфейс и dynamic в реализацию.

+ (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)]; };
}

Саму библиотеку можно найти на гитхабе, а я готов ответить на ваши вопросы в комментариях.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть