Могу ли я передать блок как @selector с Objective-C?
Можно ли передать блок Objective-C для @selector
аргумент в UIButton
? есть ли способ заставить работать следующее?
[closeOverlayButton addTarget:self
action:^ {[anotherIvarLocalToThisMethod removeFromSuperview];}
forControlEvents:UIControlEventTouchUpInside];
Спасибо
9 ответов
Да, но вы должны использовать категорию.
Что-то вроде:
@interface UIControl (DDBlockActions)
- (void) addEventHandler:(void(^)(void))handler
forControlEvents:(UIControlEvents)controlEvents;
@end
Реализация будет немного сложнее:
#import <objc/runtime.h>
@interface DDBlockActionWrapper : NSObject
@property (nonatomic, copy) void (^blockAction)(void);
- (void) invokeBlock:(id)sender;
@end
@implementation DDBlockActionWrapper
@synthesize blockAction;
- (void) dealloc {
[self setBlockAction:nil];
[super dealloc];
}
- (void) invokeBlock:(id)sender {
[self blockAction]();
}
@end
@implementation UIControl (DDBlockActions)
static const char * UIControlDDBlockActions = "unique";
- (void) addEventHandler:(void(^)(void))handler
forControlEvents:(UIControlEvents)controlEvents {
NSMutableArray * blockActions =
objc_getAssociatedObject(self, &UIControlDDBlockActions);
if (blockActions == nil) {
blockActions = [NSMutableArray array];
objc_setAssociatedObject(self, &UIControlDDBlockActions,
blockActions, OBJC_ASSOCIATION_RETAIN);
}
DDBlockActionWrapper * target = [[DDBlockActionWrapper alloc] init];
[target setBlockAction:handler];
[blockActions addObject:target];
[self addTarget:target action:@selector(invokeBlock:) forControlEvents:controlEvents];
[target release];
}
@end
Некоторое объяснение:
- Мы используем пользовательский класс "only only"
DDBlockActionWrapper
, Это простой класс, который имеет свойство блока (блок, который мы хотим вызвать) и метод, который просто вызывает этот блок. -
UIControl
Категория просто создает экземпляр одной из этих оболочек, дает ей блок для вызова, а затем говорит себе использовать эту оболочку и ееinvokeBlock:
метод как цель и действие (как обычно). -
UIControl
Категория использует связанный объект для хранения массиваDDBlockActionWrappers
, так какUIControl
не сохраняет свои цели. Этот массив должен гарантировать, что блоки существуют, когда они должны быть вызваны. -
Мы должны убедиться, чтоDDBlockActionWrappers
убираться, когда объект уничтожен, поэтому мы делаем отвратительный вздор-[UIControl dealloc]
с новым, который удаляет связанный объект, а затем вызывает оригиналdealloc
код.Хитрый, хитрый.На самом деле, связанные объекты очищаются автоматически во время освобождения.
Наконец, этот код был набран в браузере и не был скомпилирован. Вероятно, с этим что-то не так. Ваш пробег может отличаться.
Блоки являются объектами. Передайте свой блок как target
аргумент, с @selector(invoke)
как action
аргумент, как это:
id block = [^{NSLog(@"Hello, world");} copy];// Don't forget to -release.
[button addTarget:block
action:@selector(invoke)
forControlEvents:UIControlEventTouchUpInside];
Нет, селекторы и блоки не являются совместимыми типами в Objective-C (на самом деле, это очень разные вещи). Вам придется написать свой собственный метод и вместо него передать его селектор.
Можно ли передать блок Objective-C для аргумента @selector в кнопке UIB?
Принимая во внимание все уже предоставленные ответы, ответ "Да", но для настройки некоторых категорий требуется небольшая работа.
Я рекомендую использовать NSInvocation, потому что вы можете многое сделать с этим, например, с таймерами, хранящимися как объекты и вызываемыми... и т.д...
Вот что я сделал, но учтите, что я использую ARC.
Первый - это простая категория по NSObject:
.час
@interface NSObject (CategoryNSObject)
- (void) associateValue:(id)value withKey:(NSString *)aKey;
- (id) associatedValueForKey:(NSString *)aKey;
@end
.m
#import "Categories.h"
#import <objc/runtime.h>
@implementation NSObject (CategoryNSObject)
#pragma mark Associated Methods:
- (void) associateValue:(id)value withKey:(NSString *)aKey {
objc_setAssociatedObject( self, (__bridge void *)aKey, value, OBJC_ASSOCIATION_RETAIN );
}
- (id) associatedValueForKey:(NSString *)aKey {
return objc_getAssociatedObject( self, (__bridge void *)aKey );
}
@end
Следующая категория для NSInvocation для хранения в блоке:
.час
@interface NSInvocation (CategoryNSInvocation)
+ (NSInvocation *) invocationWithTarget:(id)aTarget block:(void (^)(id target))block;
+ (NSInvocation *) invocationWithSelector:(SEL)aSelector forTarget:(id)aTarget;
+ (NSInvocation *) invocationWithSelector:(SEL)aSelector andObject:(__autoreleasing id)anObject forTarget:(id)aTarget;
@end
.m
#import "Categories.h"
typedef void (^BlockInvocationBlock)(id target);
#pragma mark - Private Interface:
@interface BlockInvocation : NSObject
@property (readwrite, nonatomic, copy) BlockInvocationBlock block;
@end
#pragma mark - Invocation Container:
@implementation BlockInvocation
@synthesize block;
- (id) initWithBlock:(BlockInvocationBlock)aBlock {
if ( (self = [super init]) ) {
self.block = aBlock;
} return self;
}
+ (BlockInvocation *) invocationWithBlock:(BlockInvocationBlock)aBlock {
return [[self alloc] initWithBlock:aBlock];
}
- (void) performWithTarget:(id)aTarget {
self.block(aTarget);
}
@end
#pragma mark Implementation:
@implementation NSInvocation (CategoryNSInvocation)
#pragma mark - Class Methods:
+ (NSInvocation *) invocationWithTarget:(id)aTarget block:(void (^)(id target))block {
BlockInvocation *blockInvocation = [BlockInvocation invocationWithBlock:block];
NSInvocation *invocation = [NSInvocation invocationWithSelector:@selector(performWithTarget:) andObject:aTarget forTarget:blockInvocation];
[invocation associateValue:blockInvocation withKey:@"BlockInvocation"];
return invocation;
}
+ (NSInvocation *) invocationWithSelector:(SEL)aSelector forTarget:(id)aTarget {
NSMethodSignature *aSignature = [aTarget methodSignatureForSelector:aSelector];
NSInvocation *aInvocation = [NSInvocation invocationWithMethodSignature:aSignature];
[aInvocation setTarget:aTarget];
[aInvocation setSelector:aSelector];
return aInvocation;
}
+ (NSInvocation *) invocationWithSelector:(SEL)aSelector andObject:(__autoreleasing id)anObject forTarget:(id)aTarget {
NSInvocation *aInvocation = [NSInvocation invocationWithSelector:aSelector
forTarget:aTarget];
[aInvocation setArgument:&anObject atIndex:2];
return aInvocation;
}
@end
Вот как это использовать:
NSInvocation *invocation = [NSInvocation invocationWithTarget:self block:^(id target) {
NSLog(@"TEST");
}];
[invocation invoke];
Вы можете многое сделать с помощью вызова и стандартных методов Objective-C. Например, вы можете использовать NSInvocationOperation (initWithInvocation:), NSTimer (scheduleTimerWithTimeInterval:invocation:repeatates:)
Дело в том, что превращение вашего блока в NSInvocation более универсально и может использоваться следующим образом:
NSInvocation *invocation = [NSInvocation invocationWithTarget:self block:^(id target) {
NSLog(@"My Block code here");
}];
[button addTarget:invocation
action:@selector(invoke)
forControlEvents:UIControlEventTouchUpInside];
Опять же, это только одно предложение.
К сожалению, не все так просто.
Теоретически можно было бы определить функцию, которая динамически добавляет метод к классу target
пусть этот метод выполнит содержимое блока и вернет селектор по необходимости action
аргумент. Эта функция может использовать технику, используемую MABlockClosure, которая, в случае iOS, зависит от пользовательской реализации libffi, которая все еще является экспериментальной.
Вам лучше реализовывать действие как метод.
Кто-то скажет мне, почему это неправильно, может быть, или, если повезет, а может и нет, поэтому я либо чему-то научусь, либо помогу.
Я просто бросил это вместе. Это действительно просто, просто тонкая обертка с небольшим количеством заливки. Предупреждающее слово: предполагается, что вызываемый вами блок имеет правильную сигнатуру, соответствующую выбранному вами селектору (то есть количеству аргументов и типов).
//
// BlockInvocation.h
// BlockInvocation
//
// Created by Chris Corbyn on 3/01/11.
// Copyright 2011 __MyCompanyName__. All rights reserved.
//
#import <Cocoa/Cocoa.h>
@interface BlockInvocation : NSObject {
void *block;
}
-(id)initWithBlock:(void *)aBlock;
+(BlockInvocation *)invocationWithBlock:(void *)aBlock;
-(void)perform;
-(void)performWithObject:(id)anObject;
-(void)performWithObject:(id)anObject object:(id)anotherObject;
@end
А также
//
// BlockInvocation.m
// BlockInvocation
//
// Created by Chris Corbyn on 3/01/11.
// Copyright 2011 __MyCompanyName__. All rights reserved.
//
#import "BlockInvocation.h"
@implementation BlockInvocation
-(id)initWithBlock:(void *)aBlock {
if (self = [self init]) {
block = (void *)[(void (^)(void))aBlock copy];
}
return self;
}
+(BlockInvocation *)invocationWithBlock:(void *)aBlock {
return [[[self alloc] initWithBlock:aBlock] autorelease];
}
-(void)perform {
((void (^)(void))block)();
}
-(void)performWithObject:(id)anObject {
((void (^)(id arg1))block)(anObject);
}
-(void)performWithObject:(id)anObject object:(id)anotherObject {
((void (^)(id arg1, id arg2))block)(anObject, anotherObject);
}
-(void)dealloc {
[(void (^)(void))block release];
[super dealloc];
}
@end
Там действительно ничего волшебного не происходит. Просто много удручает void *
и приведение типа к пригодной для использования подписи блока перед вызовом метода. Очевидно (так же, как с performSelector:
и связанный метод, возможные комбинации входов конечны, но расширяемы, если вы измените код.
Используется так:
BlockInvocation *invocation = [BlockInvocation invocationWithBlock:^(NSString *str) {
NSLog(@"Block was invoked with str = %@", str);
}];
[invocation performWithObject:@"Test"];
Это выводит:
2011-01-03 16: 11: 16.020 BlockInvocation [37096: a0f] Блок был вызван с помощью str = Test
Используемый в сценарии целевого действия, вам просто нужно сделать что-то вроде этого:
BlockInvocation *invocation = [[BlockInvocation alloc] initWithBlock:^(id sender) {
NSLog(@"Button with title %@ was clicked", [(NSButton *)sender title]);
}];
[myButton setTarget:invocation];
[myButton setAction:@selector(performWithObject:)];
Поскольку цель в системе целевого действия не сохраняется, вам необходимо убедиться, что вызывающий объект живет так же долго, как и сам элемент управления.
Мне интересно что-нибудь услышать от кого-то более опытного, чем я.
Мне нужно было действие, связанное с UIButton внутри UITableViewCell. Я хотел избежать использования тегов для отслеживания каждой кнопки в каждой отдельной ячейке. Я думал, что самый прямой способ добиться этого - связать блок "действие" с кнопкой следующим образом:
[cell.trashButton addTarget:self withActionBlock:^{
NSLog(@"Will remove item #%d from cart!", indexPath.row);
...
}
forControlEvent:UIControlEventTouchUpInside];
Моя реализация немного упрощена, спасибо @bbum за упоминание imp_implementationWithBlock
а также class_addMethod
(хотя и не проверено всесторонне):
#import <objc/runtime.h>
@implementation UIButton (ActionBlock)
static int _methodIndex = 0;
- (void)addTarget:(id)target withActionBlock:(ActionBlock)block forControlEvent:(UIControlEvents)controlEvents{
if (!target) return;
NSString *methodName = [NSString stringWithFormat:@"_blockMethod%d", _methodIndex];
SEL newMethodName = sel_registerName([methodName UTF8String]);
IMP implementedMethod = imp_implementationWithBlock(block);
BOOL success = class_addMethod([target class], newMethodName, implementedMethod, "v@:");
NSLog(@"Method with block was %@", success ? @"added." : @"not added." );
if (!success) return;
[self addTarget:target action:newMethodName forControlEvents:controlEvents];
// On to the next method name...
++_methodIndex;
}
@end
Разве это не работает, чтобы иметь NSBlockOperation (iOS SDK +5). Этот код использует ARC, и это упрощение приложения, с которым я тестирую его (кажется, работает, по крайней мере, очевидно, не уверен, что у меня происходит утечка памяти).
NSBlockOperation *blockOp;
UIView *testView;
-(void) createTestView{
UIView *testView = [[UIView alloc] initWithFrame:CGRectMake(0, 60, 1024, 688)];
testView.backgroundColor = [UIColor blueColor];
[self.view addSubview:testView];
UIButton *btnBack = [UIButton buttonWithType:UIButtonTypeRoundedRect];
[btnBack setFrame:CGRectMake(200, 200, 200, 70)];
[btnBack.titleLabel setText:@"Back"];
[testView addSubview:btnBack];
blockOp = [NSBlockOperation blockOperationWithBlock:^{
[testView removeFromSuperview];
}];
[btnBack addTarget:blockOp action:@selector(start) forControlEvents:UIControlEventTouchUpInside];
}
Конечно, я не уверен, насколько это хорошо для реального использования. Вам нужно сохранить ссылку на NSBlockOperation, или я думаю, что ARC убьет его.