Могу ли я передать блок как @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 его убьет.