ฉันสามารถส่งบล็อกเป็น @selector ด้วย Objective-C ได้หรือไม่


90

เป็นไปได้ไหมที่จะส่งผ่าน Objective-C block สำหรับ@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หมวดหมู่เพียง instantiates หนึ่งห่อเหล่านี้ให้มันบล็อกที่จะเรียกแล้วบอกตัวเองว่าจะใช้กระดาษห่อและinvokeBlock:วิธีการที่เป็นเป้าหมายและการกระทำ (ตามปกติ)
  3. UIControlประเภทการใช้วัตถุที่เกี่ยวข้องในการจัดเก็บอาร์เรย์ของDDBlockActionWrappersเพราะUIControlไม่เก็บเป้าหมาย อาร์เรย์นี้มีไว้เพื่อให้แน่ใจว่าบล็อกมีอยู่เมื่อควรจะเรียกใช้
  4. เราต้องตรวจสอบให้แน่ใจว่าการDDBlockActionWrappersล้างข้อมูลเมื่อวัตถุถูกทำลายดังนั้นเราจึงทำการแฮ็คที่น่ารังเกียจ-[UIControl dealloc]โดยใช้วัตถุใหม่ที่ลบวัตถุที่เกี่ยวข้องออกจากนั้นจึงเรียกใช้deallocรหัสเดิม หากินยาก. ที่จริงวัตถุที่เกี่ยวข้องมีการทำความสะอาดขึ้นโดยอัตโนมัติในระหว่าง deallocation

สุดท้ายโค้ดนี้ถูกพิมพ์ลงในเบราว์เซอร์และยังไม่ได้คอมไพล์ อาจมีบางอย่างผิดปกติกับมัน ระยะทางของคุณอาจแตกต่างกันไป


4
โปรดทราบว่าตอนนี้คุณสามารถใช้objc_implementationWithBlock()และclass_addMethod()แก้ไขปัญหานี้ได้อย่างมีประสิทธิภาพมากกว่าการใช้วัตถุที่เกี่ยวข้องเล็กน้อย (ซึ่งหมายความว่าการค้นหาแฮชที่ไม่มีประสิทธิภาพเท่ากับการค้นหาวิธีการ) น่าจะเป็นความแตกต่างของประสิทธิภาพที่ไม่เกี่ยวข้อง แต่ก็เป็นอีกทางเลือกหนึ่ง
bbum

@bbum คุณหมายถึงimp_implementationWithBlock?
vikingosegundo

ใช่ - อันนั้น objc_implementationWithBlock()มันเป็นชื่อหนึ่งครั้ง :)
bbum

การใช้สิ่งนี้สำหรับปุ่มในแบบกำหนดเองUITableViewCellจะส่งผลให้เกิดการซ้ำซ้อนของเป้าหมาย-actions ที่ต้องการเนื่องจากเป้าหมายใหม่ทุกรายการเป็นอินสแตนซ์ใหม่และรายการก่อนหน้าจะไม่ได้รับการล้างข้อมูลสำหรับเหตุการณ์เดียวกัน คุณต้องทำความสะอาดเป้าหมายก่อน 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 ส่วนตัว
Brian

1
ทำงานเมื่อส่งผ่านnilแทน@selector(invoke).
k06a

18

ไม่ตัวเลือกและบล็อกไม่ใช่ประเภทที่เข้ากันได้ใน Objective-C (อันที่จริงมันต่างกันมาก) คุณจะต้องเขียนวิธีการของคุณเองและส่งตัวเลือกแทน


11
โดยเฉพาะอย่างยิ่งตัวเลือกไม่ใช่สิ่งที่คุณดำเนินการ เป็นชื่อของข้อความที่คุณส่งไปยังวัตถุ (หรือมีวัตถุอื่นส่งไปยังวัตถุที่สามเช่นในกรณีนี้คุณกำลังบอกให้ตัวควบคุมส่งข้อความ [ตัวเลือกไปที่นี่] ไปยังเป้าหมาย) ในทางกลับกันบล็อกคือสิ่งที่คุณดำเนินการ: คุณเรียกว่าบล็อกโดยตรงโดยไม่ขึ้นกับวัตถุ
Peter Hosey

8

เป็นไปได้ไหมที่จะส่งผ่านบล็อก 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];

นี่เป็นเพียงคำแนะนำเดียว


อีกสิ่งหนึ่งที่เรียกร้องที่นี่เป็นวิธีการสาธารณะ developer.apple.com/library/mac/#documentation/Cocoa/Reference/…
Arvin

5

ไม่ง่ายอย่างนั้นน่าเสียดาย

ตามทฤษฎีแล้วมันจะเป็นไปได้ที่จะกำหนดฟังก์ชันที่เพิ่มวิธีการแบบไดนามิกให้กับคลาสของtargetโดยให้เมธอดนั้นดำเนินการเนื้อหาของบล็อกและส่งคืนตัวเลือกตามที่actionอาร์กิวเมนต์ต้องการ ฟังก์ชันนี้สามารถใช้เทคนิคที่ใช้โดยMABlockClosureซึ่งในกรณีของ iOS นั้นขึ้นอยู่กับการใช้งาน libffi แบบกำหนดเองซึ่งยังอยู่ระหว่างการทดลอง

คุณควรใช้การดำเนินการเป็นวิธีการดีกว่า


4

ไลบรารีBlocksKitบน Github (พร้อมใช้งานในรูปแบบ CocoaPod) มีคุณสมบัตินี้ในตัว

ดูไฟล์ส่วนหัวสำหรับ UIControl + BlocksKit.h พวกเขานำแนวคิดของ Dave DeLong มาใช้ดังนั้นคุณไม่จำเป็นต้องทำ เอกสารบางอย่างที่นี่


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 จะฆ่ามัน

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.