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.
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()
}