Home > Enterprise >  Interesting confusion assigning an optional protocol method to a variable
Interesting confusion assigning an optional protocol method to a variable

Time:10-24

Let's say I have a UITextField. I want to check if its delegate is set, and if so, check to see if a specific optional method is implemented. If it is implemented, then I need to call it, otherwise perform a fallback action. A common way to do this is:

let field = // some UITextField instance
if let delegate = field.delegate, delegate.responds(to: #selector(UITextFieldDelegate.textField(_:shouldChangeCharactersIn:replacementString:))) {
    if delegate.textField?(field, shouldChangeCharactersIn: someRange, replacementString: someString) == true {
        // do something here
    }
} else {
    // Fallback action
}

That is all working. But then I had an urge to try a different approach. Instead of using responds(to:) I want to assign the optional delegate method to a variable. I came up with the following which actually works:

Note: The following code requires a deployment target of iOS 15.0 to compile. If you set the target to iOS 16.0 then it doesn't compile. Not sure why.

if let delegate = field.delegate, let should = delegate.textField {
    if should(field, field.selectedRange, title) {
        // do something here
    }
} else {
    // Fallback action
}

While this works I'm really confused why it works.

  1. The let should = delegate.textField part makes no reference to the method's parameters.
  2. The UITextFieldDelegate protocol has 4 optional methods that start with textField. These are:
    • func textField(UITextField, shouldChangeCharactersIn: NSRange, replacementString: String) -> Bool
    • func textField(UITextField, editMenuForCharactersIn: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu?
    • func textField(UITextField, willDismissEditMenuWith: UIEditMenuInteractionAnimating)
    • func textField(UITextField, willPresentEditMenuWith: UIEditMenuInteractionAnimating)

So how does this work? How does the compiler know which one I meant? It actually seems to only work with the one that happens to be first.

I can't find any way to do this for the other 3 delegate methods. If I really want to call the func textField(UITextField, editMenuForCharactersIn: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu? method I can't see how. The following code will not compile:

if let delegate = field.delegate, let editMenu = delegate.textField {
    let suggested = [UIMenuElement]()
    if let menu = editMenu(field, field.selectedRange, suggested) {
    }
}

This gives the error:

Cannot convert value of type '[UIMenuElement]' to expected argument type 'String'

on the if let menu = should(field, field.selectedRange, suggested) { line. This clearly indicates it is assuming the func textField(UITextField, shouldChangeCharactersIn: NSRange, replacementString: String) -> Bool method.

Is there a syntax (that I'm missing) that allows me to assign the specific protocol method to a variable?

I'm going to stick with the tried and true use of responds(to:) for now but I'd love an explanation of what's going on with the attempts to assign the ambiguously named protocol method to a variable and if there is a way to specify the parameters to get the correct assignment.

My searching on SO didn't yield any relevant questions/answers.

My code is in an iOS project with a deployment target of iOS 15.0 and later using Xcode 14.0.1. The Swift compiler setting is set for Swift 5. It seems the code doesn't compile with a deployment target of iOS 16.0. Strange.

CodePudding user response:

You can disambiguate either by strongly typing or spelling out the parameters. Neither of us knows why your code is compiling without disambiguating. It doesn't work for us.

if
  let shouldChangeCharacters: (_, _, String) -> _ = field.delegate?.textField,
  let editMenu: (_, _, [UIMenuElement]) -> _ = field.delegate?.textField,
  let willDismissEditMenu = field.delegate?.textField(_:willDismissEditMenuWith:),
  let willPresentEditMenu = field.delegate?.textField(_:willPresentEditMenuWith:)
{ }

CodePudding user response:

I'm not sure about the actual answer to your question, but I will say that your working code doesn't compile for me if I am targeting iOS 16. I get an error that says:

error: ambiguous use of 'textField'
if let delegate = textField.delegate, let should = delegate.textField {

UIKit.UITextFieldDelegate:13:19: note: found this candidate
    optional func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool
UIKit.UITextFieldDelegate:23:19: note: found this candidate
    optional func textField(_ textField: UITextField, editMenuForCharactersIn range: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu?

But instead of holding onto the function with your should variable, why not unwrap the result as a part of your if statement?

Something like this:

if let delegate = textField.delegate,
   let should = delegate.textField?(field, shouldChangeCharactersIn: someRange, replacementString: someString) {
    if should {
        // perform some action
    }
} else {
    // perform fallback action
}

The resulting logic should be the same.

  • Related