Могу ли я передать блок как @selector с помощью Objective-C?

90

Можно ли передать блок Objective-C для @selectorаргумента в a UIButton? т.е. есть ли способ заставить работать следующее?

    [closeOverlayButton addTarget:self 
                           action:^ {[anotherIvarLocalToThisMethod removeFromSuperview];} 
                 forControlEvents:UIControlEventTouchUpInside];

Благодарность

Билл Шифф
источник

Ответы:

69

Да, но вам нужно будет использовать категорию.

Что-то типа:

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

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

Дэйв Делонг
источник
4
Обратите внимание, что теперь вы можете использовать objc_implementationWithBlock()и class_addMethod()для решения этой проблемы несколько более эффективным способом, чем использование связанных объектов (что подразумевает поиск хэша, который не так эффективен, как поиск метода). Возможно несущественная разница в производительности, но это альтернатива.
bbum
@bbum ты имеешь в виду imp_implementationWithBlock?
vikingosegundo
Ага - тот. Когда-то он был назван objc_implementationWithBlock(). :)
bbum
Использование этого для кнопок в custom UITableViewCellприведет к дублированию желаемых целей-действий, поскольку каждая новая цель является новым экземпляром, а предыдущие не очищаются для тех же событий. Вы должны сначала очистить мишени for (id t in self.allTargets) { [self removeTarget:t action:@selector(invokeBlock:) forControlEvents:controlEvents]; } [self addTarget:target action:@selector(invokeBlock:) forControlEvents:controlEvents];
Евгений
Я думаю, что одна вещь, которая делает приведенный выше код более понятным, - это знание того, что UIControl может принимать множество пар target: action ... следовательно, необходимо создать изменяемый массив для хранения всех этих пар
abbood
41

Блоки - это объекты. Пройдите свой блок в качестве targetаргумента, с @selector(invoke)как actionаргумент, как это:

id block = [^{NSLog(@"Hello, world");} copy];// Don't forget to -release.

[button addTarget:block
           action:@selector(invoke)
 forControlEvents:UIControlEventTouchUpInside];
лемнар
источник
Это интересно. Я посмотрю, смогу ли я сделать что-нибудь подобное сегодня вечером. Может начать новый вопрос.
Tad Donaghe
31
Это «работает» случайно. Он полагается на частный API; invokeметод на блочных объектах не является общедоступным и не предназначено для использования в этой моде.
bbum
1
Ббом: Ты прав. Я думал, что -invoke был общедоступным, но я хотел обновить свой ответ и сообщить об ошибке.
lemnar
1
это кажется отличным решением, но мне интересно, приемлемо ли оно для Apple, поскольку оно использует частный API.
Брайан
1
Работает при прохождении nilвместо @selector(invoke).
k06a
17

Нет, селекторы и блоки не являются совместимыми типами в Objective-C (на самом деле, это разные вещи). Вам придется написать свой собственный метод и вместо этого передать его селектор.

BoltClock
источник
11
В частности, селектор - это не то, что вы выполняете; это имя сообщения, которое вы отправляете объекту (или отправляете другой объект третьему объекту, как в этом случае: вы говорите элементу управления отправить сообщение [селектор идет сюда] цели). С другой стороны, блок - это то , что вы выполняете: вы вызываете блок напрямую, независимо от объекта.
Питер Хози,
7

Можно ли передать блок Objective-C для аргумента @selector в UIButton?

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

Я рекомендую использовать 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];

Опять же, это всего лишь одно предложение.

Арвин
источник
Еще одна вещь, здесь invoke - это общедоступный метод. developer.apple.com/library/mac/#documentation/Cocoa/Reference/…
Арвин,
5

К сожалению, не все так просто.

Теоретически можно было бы определить функцию, которая динамически добавляет метод к классу target, чтобы этот метод выполнял содержимое блока и возвращал селектор, если это необходимо actionаргументу. Эта функция может использовать технику, используемую MABlockClosure , которая в случае iOS зависит от специальной реализации libffi, которая все еще является экспериментальной.

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

Куинн Тейлор
источник
4

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

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

Нейт Кук
источник
1

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

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

//
//  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:)];

Поскольку цель в системе целевых действий не сохраняется, вам нужно будет гарантировать, что вызывающий объект живет столько же, сколько и сам элемент управления.

Мне интересно услышать что-нибудь от кого-нибудь более опытного, чем я.

d11wtq
источник
у вас есть утечка памяти в этом сценарии целевого действия, потому что invocationон никогда не выпускается
user102008
1

Мне нужно было связать действие с 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
Дон Мигель
источник
0

Разве не работает 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 убьет ее.

руфо
источник