Search code examples
objective-cswiftcocoacocoa-touchobjective-c-blocks

How to store this Objective-C block inside a Swift variable?


Here is the Objective-C block:

@property (copy) void (^anObjcBlock)();

anObjcBlock = ^{
    NSLog(@"Yea man this thing works!!");
};
NSMutableArray *theArrayThatHasTheBlockInItAtIndexZero = [NSMutableArray array];
[theArrayThatHasTheBlockInItAtIndexZero addObject:anObjBlock];

Here's what I did in Swift:

var theBlock: (()->Void)?

theBlock = theArrayThatHasTheBlockInItAtIndexZero[0] as? ()->Void
// Now call the block
theBlock!()

But with this I get runtime error.
Basically, the theBlock = theArrayThatHasTheBlockInItAtIndexZero[0] as? ()->Void statement would make theBlock nil because the as? failed. And when I changed the statement to theBlock = theArrayThatHasTheBlockInItAtIndexZero[0] as! ()->Void, I get a runtime error:

enter image description here

I'm not sure what else to do. This is an empty project, there really is no code in it.


Solution

  • It looks like the issue, in this case, comes from the NSMutableArray.

    [NSMutableArray objectAtIndex:] returns id in Objective-C, which gets translated to AnyObject by Swift.

    You will get an error if you attempt to cast AnyObject to () ->Void.

    A workaround is the following:

    // Create your own typealias (we need this for unsafeBitcast)
    typealias MyType = @convention(block) () -> Void
    
    // Get the Obj-C block as AnyObject
    let objcBlock : AnyObject = array.firstObject! // or [0]
    
    // Bitcast the AnyObject Objective-C block to a "Swifty" Objective-C block (@convention(block)) 
    // and then assign the result to a variable of () -> Void type
    
    let block : () -> Void = unsafeBitCast(objcBlock, MyType.self)
    
    // Call the block
     
    block()
    

    This code works for me.


    FUNNY FACT

    If you edit your Objective-C code to look like this...

    // Typedef your block type
    typedef void (^MyType)();
    
    // Declare your property as NSArray of type MyType
    @property (strong) NSArray<MyType>* array;
    

    Swift will now report the array type as [MyType]!.

    For some reason, generics on NSMutableArray doesn't seem to be picked up by Swift.

    Despite that, you'll get a runtime error if you execute:

    let block : MyType? = array[0]