Могу ли я передать блок как @selector с помощью Objective-C?
Можно ли передать блок Objective-C для аргумента @selector
в UIButton
? то есть, есть ли способ заставить следующее работать?
[closeOverlayButton addTarget:self
action:^ {[anotherIvarLocalToThisMethod removeFromSuperview];}
forControlEvents:UIControlEventTouchUpInside];
Спасибо
Ответы
Ответ 1
Да, но вам нужно будет использовать категорию.
Что-то вроде:
@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
Некоторое объяснение:
- Мы используем пользовательский класс "только для внутреннего использования"
DDBlockActionWrapper
. Это простой класс, который имеет свойство блока (блок, который мы хотим вызвать), и метод, который просто вызывает этот блок.
- Категория
UIControl
просто создает экземпляр одной из этих оболочек, дает ему блок, который вызывается, а затем говорит себе использовать эту оболочку и ее метод invokeBlock:
как цель и действие (как обычно).
- В категории
UIControl
используется связанный объект для хранения массива DDBlockActionWrappers
, потому что UIControl
не сохраняет свои цели. Этот массив должен гарантировать, что блоки существуют, когда они должны быть вызваны.
-
Мы должны убедиться, что DDBlockActionWrappers
очищается, когда объект уничтожен, поэтому мы делаем неприятный взлом -[UIControl dealloc]
с новым, который удаляет связанный объект, а затем вызывает исходный код dealloc
. Tricky, tricky. Фактически, связанные объекты очищаются автоматически во время освобождения.
Наконец, этот код был введен в браузере и не был скомпилирован. Вероятно, с ним что-то не так. Ваш пробег может отличаться.
Ответ 2
Блоки - это объекты. Передайте свой блок как аргумент target
, с @selector(invoke)
в качестве аргумента action
, например:
id block = [^{NSLog(@"Hello, world");} copy];// Don't forget to -release.
[button addTarget:block
action:@selector(invoke)
forControlEvents:UIControlEventTouchUpInside];
Ответ 3
Нет, селектора и блоки не являются совместимыми типами в Objective-C (на самом деле это разные вещи). Вам придется написать свой собственный метод и вместо этого передать его селектор.
Ответ 4
Можно ли передать блок Objective-C для аргумента @selector в UIButton?
Принимая во всех уже предоставленных ответах, ответ "Да", но для настройки некоторых категорий требуется небольшая работа.
Я рекомендую использовать NSInvocation, потому что вы можете многое сделать с этим, например, с таймерами, сохраненными как объект и вызываемыми... и т.д.
Вот что я сделал, но заметьте, что я использую ARC.
Сначала это простая категория в NSObject:
.h
@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 для хранения в блоке:
.h
@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: repeat:)
Точка превращает ваш блок в NSInvocation более универсальна и может использоваться как таковая:
NSInvocation *invocation = [NSInvocation invocationWithTarget:self block:^(id target) {
NSLog(@"My Block code here");
}];
[button addTarget:invocation
action:@selector(invoke)
forControlEvents:UIControlEventTouchUpInside];
Снова это только одно предложение.
Ответ 5
Не так просто, к сожалению.
В теории можно было бы определить функцию, динамически добавляющую метод к классу target
, чтобы этот метод выполнял содержимое блока и возвращал селектор по мере необходимости аргументом action
. Эта функция может использовать технику, используемую MABlockClosure, которая в случае iOS зависит от пользовательской реализации libffi, которая все еще экспериментальные.
Вам лучше реализовать действие как метод.
Ответ 6
В библиотеке BlocksKit в Github (также доступной как CocoaPod) встроена эта функция.
Взгляните на файл заголовка для UIControl + BlocksKit.h. Они внедрили идею Дейва ДеЛонга, так что вам не нужно. Некоторая документация здесь.
Ответ 7
Кто-то скажет мне, почему это неправильно, может быть, или с какой-либо удачей, может быть, нет, поэтому я либо узнаю что-то, либо я буду полезен.
Я просто выбросил это вместе. Это действительно базовая, просто тонкая обертка с немного отливкой. Предупреждающее слово предполагает, что блок, который вы вызываете, имеет правильную подпись в соответствии с используемым вами селектором (т.е. Количеством аргументов и типов).
//
// 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
Там действительно ничего волшебного не происходит. Просто много downcasting до void *
и typecasting к полезной блочной сигнатуре перед вызовом метода. Очевидно (как и при использовании 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:)];
Поскольку цель в системе целевого действия не сохраняется, вам нужно будет обеспечить, чтобы объект-вызов продолжался до тех пор, пока сам элемент управления.
Мне интересно услышать что-нибудь от кого-то более экспертного, чем я.
Ответ 8
Мне нужно было выполнить действие, связанное с 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, "[email protected]:");
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
Ответ 9
Не работает ли 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 его убьет.