Чи можу я передати блок як @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

Використання цього для кнопок у користувацьких UITableViewCellпризведе до дублювання бажаних цільових дій, оскільки кожна нова ціль є новим екземпляром, а попередні не очищаються для тих самих подій. Спершу потрібно очистити цілі for (id t in self.allTargets) { [self removeTarget:t action:@selector(invokeBlock:) forControlEvents:controlEvents]; } [self addTarget:target action:@selector(invokeBlock:) forControlEvents:controlEvents];
Євген

Я думаю, що одне, що робить вищезгаданий код більш зрозумілим, це знання того, що UIControl може приймати багато пар ціль: дії .. звідси необхідність створити змінний масив для зберігання всіх цих пар
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
Bbum: Ви маєте рацію. Я думав, що -invoke є загальнодоступним, але я мав намір оновити свою відповідь та виправити помилку.
lemnar

1
це здається чудовим рішенням, але мені цікаво, чи прийнятно це для Apple, оскільки воно використовує приватний API.
Брайан

1
Працює при передачі nilзамість @selector(invoke).
k06a

17

Ні, селектори та блоки не є сумісними типами в Objective-C (насправді це дуже різні речі). Вам доведеться написати власний метод і натомість передати його селектор.


11
Зокрема, селектор - це не те, що ви виконуєте; це ім'я повідомлення, яке ви надсилаєте об'єкту (або маєте надіслати інший об'єкт третьому об'єкту, як у цьому випадку: ви говорите органу управління надіслати [селектор йде сюди] повідомлення цілі). З іншого боку, блок - це те, що ви виконуєте: Ви викликаєте блок безпосередньо, незалежно від об’єкта.
Пітер Хосі

7

Чи можна передати блок 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

Далі йде категорія NS NS, яка зберігається в блоці:

.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 (rasporedTimerWithTimeInterval: виклик: повторюється :)

Справа в тому, щоб перетворити ваш блок на NSInvocation, який є більш універсальним і може використовуватися як такий:

NSInvocation *invocation = [NSInvocation invocationWithTarget:self block:^(id target) {
                NSLog(@"My Block code here");
            }];
[button addTarget:invocation
           action:@selector(invoke)
 forControlEvents:UIControlEventTouchUpInside];

Знову ж таки, це лише одна пропозиція.


Ще одна річ, посилайтеся тут на публічний метод. 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:)];

Оскільки ціль у системі цільової дії не зберігається, вам потрібно буде забезпечити життя об’єкта виклику стільки часу, скільки виконує сам контроль.

Мені цікаво почути щось від когось більш експертного, ніж я.


у вас є витік пам’яті за цим сценарієм цільової дії, оскільки 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 його вб’є.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.