Swift closure in array becomes nil in Objective-c

北城以北 提交于 2019-12-18 09:04:37

问题


I created an objective-c method which will invoke a method via NSInvocation:

typedef void (^ScriptingEmptyBlock)();
typedef void (^ScriptingErrorBlock)(NSError *error);
- (void)scripting_execute:(NSString *)operation withParams:(nullable NSArray *)args {

    SEL selector = [self scripting_selectorForOperation:operation];
    Class class = [self class];
    NSMethodSignature *signature = [class instanceMethodSignatureForSelector:selector];
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];

    [invocation setSelector:selector];
    [invocation setTarget:self];
    for (int idx = 0; idx < args.count; idx ++) {
        id arg = args[idx];
        [invocation setArgument:&arg atIndex:idx + 2];

    }

    ScriptingEmptyBlock success = args[1];

    // Breakpoint added on next line to test for nil
    success(); // this is nil and would crash!
    // (lldb) po args.count
    // 3

    // (lldb) po success
    // Printing description of success:
    // (ScriptingEmptyBlock) success = 0x0000000000000000

    // (lldb) po args[1]
    // (Function)

    //[invocation getArgument:&success atIndex:2]; // also tried this and got nil as well
    [invocation invoke];

}

The method takes an "operation" which is translated into a selector by overriding scripting_selectorForOperation: in subclasses and then performs the invocation.

All of that works, except when the invoked method has block arguments they are nil, I added the test for nil I describe with comments, when attempting to read the closure from the array it will be nil.

Called like:

let successClosure: ScriptingEmptyBlock = {
                    print("Renamed product")
                }
let errorClosure: ScriptingErrorBlock = { error in
                    print("Failed to rename product: \(error)")
                }
let params:[Any] = [ "testName", successClosure, errorClosure]
object.scripting_execute (ScriptOperation.updateProductName.rawValue, withParams: params)

Why is closure becoming nil?


回答1:


success is not nil (in fact, NSArray cannot contain nils). If you print it like NSLog(@"%@", success);, it will say (Function), not (null). And if you print its class like NSLog(@"%@", [success class]);, it will say _SwiftValue. Basically, it is a Swift value that is bridged into Objective-C.

The problem is that the object success points to is not an Objective-C block. It is a Swift closure, and Swift closures are not the same as Objective-C blocks. Trying to use invoke it as if it were an Objective-C block causes undefined behavior. po in the debugger prints it wrong probably because it is printing it assuming it were type ScriptingEmptyBlock (a block type). If you do po (id) success, it will print (Function).

As to how you can explicitly put an Objective-C block into the array from Swift, the only way I figured out to do it something like:

let params:[Any] = [ "testName",
                     successClosure as (@convention(block) () -> Void)!,
                     errorClosure as (@convention(block) (NSError) -> Void)!]
object.scripting_execute (ScriptOperation.updateProductName.rawValue,
                          withParams: params)

I am not sure why it's necessary to put the function type inside a !, but it doesn't seem to work otherwise. Maybe someone else can find a better way.




回答2:


I must admit that I don't fully understand why this is happening, but as far as I can tell this has nothing to do with using NSInvocation and would happen even if we just passed a Swift closure to an Objective-C function via a parameter of type id. Passing an Objective-C block via id works just fine, not sure why: Swift closures are supposed to be compatible with Objective-C blocks. As you know, elements of NSArray are of type id, so any Objective-C object can be an array element.

To work around this problem of accessing a Swift closure passed via id in Objective-C one can introduce a wrapper class.

// In a header:
@interface EmptyBlockWrapper : NSObject
@property EmptyBlock _blk;
@end

// In an implementation file (just an empty implementation):
@implementation EmptyBlockWrapper

@end

Then we can use a wrapper instance instead of a block as an array element in Swift:

let myBlock : EmptyBlock = {
    print("In Swift EmptyBlock...")
}

let myBlockWrapper = EmptyBlockWrapper()
myBlockWrapper._blk = myBlock

In an Objective-C method we can call it as follows, assuming args is NSArray *:

EmptyBlockWrapper * emptyBlockWrapper = args[1];
emptyBlockWrapper._blk();

Hopefully this is helpful. Of course, this is just a simplified example to give you an idea; this could be made much fancier.



来源:https://stackoverflow.com/questions/44684601/swift-closure-in-array-becomes-nil-in-objective-c

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!