Swift closure in array becomes null in Objective-c

I created an objective-c method that will call the 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 invoked.

This all works, except when the called method has block arguments, they are zero, I added a test for nil, which I describe with comments, when trying to read the closure from the array it will be zero.

Called as:

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 does the close go to zero?

+3


source to share


2 answers


success

not nil

(actually NSArray

cannot contain nil

s). If you type it as NSLog(@"%@", success);

, it will say (Function)

, not (null)

. And if you type your class like NSLog(@"%@", [success class]);

it will say _SwiftValue

. Basically, it's a Swift value that connects to Objective-C.

The problem is that the object is success

indicating that it is not an Objective-C block. This is a quick closure, and Swift closure is not the same as Objective-C. Trying to use it as if it were an Objective-C block caused undefined behavior. po

in the debugger it doesn't display it correctly because it prints it assuming it's a type ScriptingEmptyBlock

(block type). If you do po (id) success

it will print (Function)

.

As for how you can explicitly put an Objective-C block in an array from Swift, the only way I have decided to do it is something like this:



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

      

I'm not sure why the function type needs to be injected internally !

, but it doesn't seem to work. Maybe someone else can find a better way.

+3


source


I have to admit that I don't quite understand why this is happening, but as far as I can tell it has nothing to do with use NSInvocation

and will happen even if we just pass Swift's closure to Objective-C via a type parameter id

. Passing an Objective-C block using id

works great, not sure why: Swift closures must be compatible with Objective-C blocks. As you know, elements NSArray

are of type id

, so any Objective-C object can be an element of an array.

To work around this issue of Swift's traversal closure access 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

      

We can then 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 Objective-C method, we can call it like this, assuming args

NSArray *

:

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

      

Hope this helps. Of course, this is just a simplified example to give you an idea; it can become much more attractive.

0


source







All Articles