I have an objc method I'd like to call from swift, but the method exists in a third party framework that may or may not exist at runtime.
What I'd like to do is call it dynamically, so I'm looking into interacting with it using selectors.
The method signature looks like this in ObjC
- (NSString * _Nullable)buildData:(NSError * _Nullable __autoreleasing * _Nullable)error;
Removing the nullable annotations, it's the bog standard "return a thing, or error" pattern that ObjC has had forever.
- (NSString*)buildData:(NSError*)error;
If swift can load this at compile time, then it quite happily translates into
func buildData() throws -> String
However, I'd like to call it dynamically. I've worked out I can do this:
let _target:NSObject = // obtain a reference to the underlying value
var error: NSError? = nil
_target.perform(NSSelectorFromString("buildData:"), with: &error)
The problem is, I can't pass a reference to the perform selector method. XCode gives me a compile error of
'&' used with non-inout argument of type 'Any?'
So my question is, how can I call this method using selectors?
Try this:
var error: NSError? = nil
let _target: NSObject = // obtain a reference to the underlying value
withUnsafeMutablePointer(to: &error) {
let selector: Selector = NSSelectorFromString("buildData:")
let methodIMP: IMP! = _target.method(for: selector)
unsafeBitCast(methodIMP,to:(#convention(c)(Any?,Selector,OpaquePointer)->Void).self)(_target,selector,OpaquePointer($0))
}
More info on using convention(c) for invoking selectors in Swift in my answer here
Related
I have
OBJC:
- (void)doSomething:(void (^)(NSError *))block;
SWIFT:
let test = Test()
test.doSomething(<#T##block: ((Error?) -> Void)!##((Error?) -> Void)!##(Error?) -> Void#>)
I would rather
try? test.doSomething { }
I would like bridging-header to translate the function into
func doSomething(block: () throws -> ()) throws {
try block()
}
Is it possible? Thanks to all!
Your Objective-C method is declaring a parameter which is a block that receives an NSError object. It's basically declaring a callback.
If your method is not asynchronous you should declare it like this:
- (BOOL)doSomething:(NSError **)error;
Now the parameter is a pointer to an NSError* object. If the method fails for some reason, it should set an appropriate error for that parameter like so:
if (error != NULL) {
*error = <an appropriate error>
}
Also note the BOOL return type. According to Cocoa conventions the caller should refer to the return type to determine if the method failed or not, instead of testing for the existence of an NSError* object.
Declaring a method like this will expose it in Swift using the throws mechanics.
Update:
I don't think you can declare a Swift throwing block in Objective-C. If you go the other way around, and declare your desired method signature in Swift you'll see the compiler complains it can't be represented in Objective-C.
Most likely the (NSError **) to throwable convention never got implemented for blocks.
In Objective-C, it's expected that you can pass in NULL to any NSError** parameter to ignore the error. However, when I try to pass NULL to a Swift method that throws an error, it generates a runtime error.
// Thrower.swift
class Thrower: NSObject {
static func throwError() throws {
throw NSError(domain: "bla", code: 0, userInfo: nil)
}
}
...
// AppDelegate.m
BOOL success = [Thrower throwErrorAndReturnError:NULL];
This generates an EXC_BAD_INSTRUCTION error, with this stack:
I'm a little surprised at this behavior. I would expect this to either work, or the compiler to generate a warning when you pass NULL to one of these methods.
Here's what the generated header of the Swift method looks like:
+ (BOOL)throwErrorAndReturnError:(NSError * __nullable * __null_unspecified)error;
If this was not supposed to work, why wouldn't they generate NSError * __nullable * __nonnull, so that a compiler warning is generated when you try to pass in a nullable NSError*?
Is there something I'm missing here, or is this just expected behavior?
On WWDC'15 Session 401 (Swift and Objective-C Interoperability) Doug Gregor said:
This means, 'I thought about it, I couldn't come to an answer.' The best thing to do is keep it implicitly unwrapped optional in Swift, keep it null-unspecified here.
So, basically, your null is mapped to ImplicitlyUnwrappedOptional<ErrorType>.None, that pretty much explains the crash.
On the session above they mention NSError ** is assumed to be nullable on both pointers. Apparently they've changed their mind or the mapping is not symmetrical, anyway it looks wrong to me.
Considering that behavior and not being NSError * __nullable * __nonnull I'd say it's a bug, I'd open a radar. If you do please let us know so we can dupe it.
[[XHAudioPlayerHelper shareInstance] setDelegate:(id<NSFileManagerDelegate>)self];
I want to translate this OC to swift.
I tried that :
XHAudioPlayerHelper.shareInstance().delegate = self as! NSFileManagerDelegate
But couldn't compiled with error of : ambiguous use of delegate,
Does shareInstance perchance return an object of type id? In that case the compiler would have no idea what type it is getting and hence which type of delegate is expected.
If so then amend it to return instancetype if possible; as a fallback perform something like:
let sharedInstance = XHAudioPlayerHelper.shareInstance() as! XHAudioPlayerHelper
sharedInstance.delegate = ...
I'm using an Objective-C class in my Swift project via a bridging header. The method signature looks something like this:
- (CFArrayRef)someMethod:(someType)someParameter;
I started by getting an instance of the class, calling the method, and storing the value:
var myInstance = MyClassWithThatMethod();
var cfArr = myInstance.someMethod(someValue);
Then try to get a value in the array:
var valueInArrayThatIWant = CFArrayGetValueAtIndex(cfArr, 0);
However I get the error Unmanaged<CFArray>' is not identical to 'CFArray'. What does Unmanaged<CFArray> even mean?
I looked through How to convert CFArray to Swift Array? but I don't need to convert the array to a swift array (however that would be nice). I just need to be able to get values from the array.
I have also tried the method of passing the CFArray into a function outlined in this answer:
func doSomeStuffOnArray(myArray: NSArray) {
}
However I get a similar error when using it:
doSomeStuffOnArray(cfArr); // Unmanaged<CFArray>' is not identical to 'NSArray'
I am using CFArray because I need to store an array of CGPathRef, which cannot be stored in NSArray.
So how am I supposed to use CFArray in Swift?
As explained in
Working with Core Foundation Types, there are two possible solutions when
you return a Core Foundation object from your own function that is imported in Swift:
Annotate the function with CF_RETURNS_RETAINED or CF_RETURNS_NOT_RETAINED.
In your case:
- (CFArrayRef)someMethod:(someType)someParameter CF_RETURNS_NOT_RETAINED;
Or convert the unmanaged object to a memory managed object with takeUnretainedValue() or takeRetainedValue() in Swift. In your case:
var cfArr = myInstance.someMethod(someValue).takeUnretainedValue()
An Unmanaged is a wrapper for an actual CF value. (Sort of like an optional.) It's there because ARC can't tell from looking at the declaration of someMethod: whether that method retains the value it returns.
You unwrap an Unmanaged by telling ARC what memory management policy to use for the value inside. If someMethod calls CFRetain on its return value:
let cfArr = myInstance.someMethod(someValue).takeRetainedValue()
If it doesn't:
let cfArr = myInstance.someMethod(someValue).takeUnretainedValue()
After you do that, cfArr is a CFArray, so you can use the bridging tricks from the other questions you linked to for accessing it like a Swift array.
If you own the code for someMethod you can change it a bit to not need this. There's a couple of options for that:
Annotate with CF_RETURNS_RETAINED or CF_RETURNS_NOT_RETAINED to tell the compiler what memory behavior is needed
Since it's an ObjC method, bridge to NSArray and return that--it'll automatically become an [AnyObject] array in Swift.
In my iOS application I need to play sound. I have the following code:
var audioFileLocationUrl = NSBundle.mainBundle().pathForResource("midnight", ofType: "m4a")
var error: NSError
var audioPlayer = AVAudioPlayer()
audioPlayer = AVAudioPlayer(contentsOfURL: audioFileLocationUrl, error: error)
audioPlayer.prepareToPlay()
On line:
audioPlayer = AVAudioPlayer(contentsOfURL: audioFileLocationUrl, error: error)
I have error: Could not find overload for 'init' that accepts the supplied arguments
Edit (it was pointed out to me that there is a clear difference between when to use NSErrorPointer and when to use NSError?. Here is the official word:
From “Using Swift with Cocoa and Objective-C.”
“When you need to report the reason for the error, you can add to the function an NSError out parameter of type NSErrorPointer. This type is roughly equivalent to Objective-C’s NSError **, with additional memory safety and optional typing. You can use the prefix & operator to pass in a reference to an optional NSError type as an NSErrorPointer object, as shown in the code listing below.
var writeError : NSError?
AVAudioPlayer(contentsOfURL: audioFileLocationUrl, error: &writeError)
Try
var error :NSError?
audioPlayer = AVAudioPlayer(contentsOfURL: audioFileLocationUrl, error: &error)
Failing that check you're calling the right method and passing in the right object types.
The reason you get this warning is because the compiler can't find that init method with those parameters.
Update: Error handling has changed drastically in Swift 2. See the documentation for the latest info.
For me, this code produces the error 'NSError' is not convertible to 'AutoreleasingUnsafePointer<NSError?>'. This is because the method is trying to take an error by reference so it can "write back" to your local variable. In Obj-C this looks like (NSError **). (Fun fact: AutoreleasingUnsafePointer<NSError?> is aliased to NSErrorPointer in Swift.)
In fact, if you ⌘-click on AVAudioPlayer to see its declaration, you'll find the init method is defined as
init(contentsOfURL url: NSURL!, error outError: AutoreleasingUnsafePointer<NSError?>)
For this reason, you must:
use an NSError? (i.e. Optional<NSError>) variable, since the error may be nil.
Pass the error in by reference with the prefix & operator.
var error: NSError?
...
audioPlayer = AVAudioPlayer(contentsOfURL: audioFileLocationUrl, error: &error)
See Error Reporting in "Using Swift with Cocoa and Objective-C" for more information.