2

Using Swift, is it possible to test if an object implements an optional protocol method without actually calling that method? This works except for cases where the optional methods differ only by their signature.

Consider this code...

@objc public protocol TestDelegate : AnyObject {
    @objc optional func testx()
    @objc optional func test(with string:String)
    @objc optional func test(with2 int:Int)
}

let delegate:TestDelegate? = nil

if let _ = delegate?.test(with:) {
    print("supports 'test(with:)'")
}

if let _ = delegate?.testx {
    print("supports 'testx'")
}

If you paste the above in a playground, it works as expected.

However, if you change testx to test, it no longer works.

Likewise, if you change test(with2) to test(with) then that won't work either.

Is there any way to test for those methods that only differ by signature?

Mark A. Donohoe
  • 28,442
  • 25
  • 137
  • 286
  • Compare https://stackoverflow.com/q/35658334/2976878 – you can explicitly coerce to disambiguate the overload you want e.g `delegate?.test as (() -> Void)?`. – Hamish Sep 24 '18 at 19:04
  • Funny. I tried that (and several variations) and couldn't get it to work. Lemme try again. – Mark A. Donohoe Sep 24 '18 at 19:27
  • Can you provide more context as to what you're trying to solve with this? It seems like an XY problem. – Alexander Sep 24 '18 at 19:50
  • Nothing to 'solve' per se. Just learning more about the language. The above was just cut/copied from a playground I was messing around in. – Mark A. Donohoe Sep 24 '18 at 21:07
  • @Hamish, I can't seem to get your answer to work with the 2nd case above (where the argument name is the same, but the type is different.) Is that possible? – Mark A. Donohoe Sep 24 '18 at 21:08
  • @MarqueIV Hmm, works fine for me: https://gist.github.com/hamishknight/27bc3abc964aadaefe7b1c7add3958a8 – Hamish Sep 24 '18 at 21:15
  • Aaah! I see what I was doing wrong. I needed the trailing '?' because it's treated as an (implicitly-unwrapped) optional. Adding the '?' at the end of the cast worked. – Mark A. Donohoe Sep 24 '18 at 21:37
  • @Hamish, mind putting that as an answer (hopefully copying over your code) so I can close this out? – Mark A. Donohoe Sep 25 '18 at 18:46
  • @MarqueIV Sure, will do when I get a moment. – Hamish Sep 25 '18 at 19:42

2 Answers2

3

Hey MarqueIV for checking the optional you can use inbuilt function

func responds(to aSelector: Selector!) -> Bool

Returns a Boolean value that indicates whether the receiver implements or inherits a method that can respond to a specified message.

The application is responsible for determining whether a false response should be considered an error.

You cannot test whether an object inherits a method from its superclass by sending responds(to:) to the object using the super keyword.

This method will still be testing the object as a whole, not just the superclass’s implementation.

Therefore, sending responds(to:) to super is equivalent to sending it to self.

Instead, you must invoke the NSObject class method instancesRespond(to:) directly on the object’s superclass, as illustrated in the following code fragment.

Listing 1

if( [MySuperclass instancesRespondToSelector:@selector(aMethod)] ) {
    // invoke the inherited method
    [super aMethod];
}

You cannot simply use [[self superclass] instancesRespondToSelector:@selector(aMethod)] since this may cause the method to fail if it is invoked by a subclass.

Note that if the receiver is able to forward aSelector messages to another object, it will be able to respond to the message, albeit indirectly, even though this method returns false. Parameters
aSelector
A selector that identifies a message. Returns true if the receiver implements or inherits a method that can respond to aSelector, otherwise false. SDKs iOS 2.0+, macOS 10.0+, tvOS 9.0+, watchOS 2.0+

Shivam Gaur
  • 491
  • 5
  • 16
  • Very knowledgeable! – Fattie Sep 25 '18 at 12:00
  • Correct me if I'm wrong, but that's Objective-C. My question was specifically around Swift where that isn't available. – Mark A. Donohoe Sep 25 '18 at 18:45
  • @MarqueIV this explanation is in Objective-C but the method I wrote in bold letters is in swift, plz check, Xcode will autosuggest it. – Shivam Gaur Sep 26 '18 at 08:22
  • Manually calling `responds(to:)` IMO isn't an idiomatically Swift way to check if a class instance implements an `@optional` protocol requirement. Such references return optional functions in order to model the fact that the conforming class may not have implemented them. – Hamish Sep 26 '18 at 19:42
1

As also shown in How do I resolve "ambiguous use of" compile error with Swift #selector syntax?, you can explicitly coerce a function reference to its expected type in order to resolve such ambiguities.

The only difference being, as such function references are to @optional protocol requirements done through optional chaining, you need to coerce to the optional type of the function. From there, you can do a comparison with nil in order to determine if both the delegate is non-nil, and it implements the given requirement.

For example:

import Foundation

@objc public protocol TestDelegate : AnyObject {
  @objc optional func test()

  // Need to ensure the requirements have different selectors.
  @objc(testWithString:) optional func test(with string: String)
  @objc(testWithInt:) optional func test(with int: Int)
}

class C : TestDelegate {
  func test() {}
  func test(with someString: String) {}
  func test(with someInt: Int) {}
}

var delegate: TestDelegate? = C()

if delegate?.test as (() -> Void)? != nil {
  print("supports 'test'")
}

if delegate?.test(with:) as ((String) -> Void)? != nil {
  print("supports 'test w/ String'")
}

if delegate?.test(with:) as ((Int) -> Void)? != nil {
  print("supports 'test w/ Int'")
}

// supports 'test'
// supports 'test w/ String'
// supports 'test w/ Int'

Note that I've given the test(with:) requirements unique selectors in order to ensure they don't conflict (this doesn't affect the disambiguation, only allowing class C to conform to TestDelegate).

Mark A. Donohoe
  • 28,442
  • 25
  • 137
  • 286
Hamish
  • 78,605
  • 19
  • 187
  • 280
  • Accepted! :) This raises a new question though. Can you disambiguate between `func test(a: String)` and `func test(b: String)`? Technically they are two different signatures, but if you can only test on the type, I'm not sure you can check without resorting to another form of interrogation. – Mark A. Donohoe Dec 09 '21 at 22:10