Home > Back-end >  Calling block from Objective C collection in Swift
Calling block from Objective C collection in Swift

Time:11-07

I have a Swift object that takes a dictionary of blocks (keyed by Strings), stores it and runs block under given key later at some point depending on external circumstances (think different behaviours depending on the backend response):

@objc func register(behaviors: [String: @convention(block) () -> Void] {
  // ...
}

It's used in a mixed-language project, so it needs to be accessible from both Swift and Objective-C. That's why there's @convention(block), otherwise compiler would complain about not being able to represent this function in Objective-C.

It works fine in Swift. But when I try to invoke it from Objective-C like that:

[behaviorManager register:@{
  @"default": ^{
    // ...
  }
}];

The code crashes and I get following error:

Could not cast value of type '__NSGlobalBlock__' (0x...) to '@convention(block) () -> ()' (0x...).

Why is that, what's going on? I thought @convention(block) is to specifically tell the compiler that Objective C blocks are going to be passed, and that's exactly what gets passed to the function in the call.

CodePudding user response:

That's why there's @convention(block), otherwise compiler would complain about not being able to represent this function in Objective-C

For the sake of consistency: commonly you use @convention attribute the other way around - when there is an interface which takes a C-pointer (and implemented in C) or an Objective-C block (and implemented in Objective-C), and you pass a Swift closure with a corresponding @convention as an argument instead (so the compiler actually can generate appropriate memory layout out of the Swift closure for the C/Objective-C implementation). So it should work perfectly fine if it's Objective-C side where the Swift-created closures are called like blocks:

@interface TDWObject : NSObject

- (void)passArguments:(NSDictionary<NSString *, void(^)()> *)params;

@end

If the class is exposed to Swift the compiler then generates corresponding signature that takes a dictionary of @convention(block) values:

func passArguments(_ params: [String : @convention(block) () -> Void])

This, however, doesn't cancel the fact that closures with @convention attribute should still work in Swift, but the things get complicated when it comes to collections, and I assume it has something with value-type vs reference-type optimisation of Swift collections. To get it round, I'd propose to make it apparent that this collection holds a reference type, by promoting it to the [String: AnyObject] and casting later on to a corresponding block type:

@objc func takeClosures(_ closures: [String: AnyObject]) {
    guard let block = closures["One"] else {
        return // the block is missing
    }
    let closure = unsafeBitCast(block, to: ObjCBlock.self)
    closure()
}

Alternatively, you may want to wrap your blocks inside of an Objective-C object, so Swift is well aware of that it's a reference type:

typedef void(^Block)();

@interface TDWBlockWrapper: NSObject

@property(nonatomic, readonly) Block block;

@end

@interface TDWBlockWrapper ()

- (instancetype)initWithBlock:(Block)block;

@end

@implementation TDWBlockWrapper

- (instancetype)initWithBlock:(Block)block {
    if (self = [super init]) {
        _block = block;
    }
    return self;
}

@end

Then for Swift it will work as simple as that:

@objc func takeBlockWrappers(_ wrappers: [String: TDWBlockWrapper]) {
    guard let wrapper = wrappers["One"] else {
        return // the block is missing
    }
    wrapper.block()
}
  • Related