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

Некоторое объяснение:

  1. Мы используем пользовательский класс "only only" DDBlockActionWrapper, Это простой класс, который имеет свойство блока (блок, который мы хотим вызвать) и метод, который просто вызывает этот блок.
  2. UIControl Категория просто создает экземпляр одной из этих оболочек, дает ей блок для вызова, а затем говорит себе использовать эту оболочку и ее invokeBlock: метод как цель и действие (как обычно).
  3. UIControl Категория использует связанный объект для хранения массива DDBlockActionWrappers, так как UIControl не сохраняет свои цели. Этот массив должен гарантировать, что блоки существуют, когда они должны быть вызваны.
  4. Мы должны убедиться, что 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, которая все еще является экспериментальной.

Вам лучше реализовывать действие как метод.

Библиотека BlocksKit на Github (также доступна как CocoaPod) имеет встроенную функцию.

Посмотрите на заголовочный файл для UIControl+BlocksKit.h. Они реализовали идею Дэйва Делонга, поэтому вам не нужно. Некоторая документация здесь.

Кто-то скажет мне, почему это неправильно, может быть, или, если повезет, а может и нет, поэтому я либо чему-то научусь, либо помогу.

Я просто бросил это вместе. Это действительно просто, просто тонкая обертка с небольшим количеством заливки. Предупреждающее слово: предполагается, что вызываемый вами блок имеет правильную сигнатуру, соответствующую выбранному вами селектору (то есть количеству аргументов и типов).

//
//  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 убьет его.

Другие вопросы по тегам