Search code examples
swiftprotocolsoption-typeoptional-arguments

In Swift, how can you test if an object implements an optional protocol method which differs by signature without actually calling that method?


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?


Solution

  • 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).