In iOS 14 how do you tap a button programmatically?

2.7k Views Asked by At

In iOS 14 I have configured a button to display a menu of questions. On its own the button is working perfectly. Here’s how it’s configured:

let button = UIButton()
button.setImage(UIImage(systemName: "chevron.down"), for: .normal)
button.showsMenuAsPrimaryAction = true
button.menu = self.menu

I have it set on a text field’s rightView. This is how it looks when the menu is displayed.

enter image description here

When a menu item is tapped, that question appears in a label just above the text field. It works great. Except….

I also want the menu to appear when the user taps in the text field. I don’t want them to have to tap on the button specifically. In the old days of target-action this was easy: button.sendActions(for: .touchUpInside). I tried that and .menuActionTriggered and .primaryActionTriggered but none of those work. I think sendActions() is looking for the old selector based actions.

But there’s a new sendActions(for: UIControl.Event) method. Here’s what I’m trying in textFieldShouldBeginEditing():

func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
    let tempAction = UIAction { action in
        print("sent action")
    }
    menuButton.sendAction(tempAction)
    print("textField tapped")
    return false
}

Sure enough, “sent action” and “textField tapped” appear in the console when the text field is tapped. But I have no idea what UIAction I could send that means “display your menu”. I wish there was this method: send(_: UIControl.Event).

Any ideas on how to get the button to display its menu when the text field is tapped?

ps. yes, textFieldShouldBeginEditing will need to know if a question has been selected and in that case will need to allow editing. That part is easy.

1

There are 1 best solutions below

2
On BEST ANSWER

From what I read, you cannot programmatically trigger UIContextMenuInteraction as interactions seem to be handled internally by itself unlike send(_: UIControl.Event)

I see this mentioned on this SO post here

Also in the docs it seems that we don't have access to interaction management Apple needs to decide 3D touch is available or default back to long tap

From the docs

A context menu interaction object tracks Force Touch gestures on devices that support 3D Touch, and long-press gestures on devices that don't support it.

Workaround

I can propose the following workaround for your use case. My example was created using frame instead of auto layout as faster and easier to demo the concept this way, however you will need to make adjustments using autolayout

1. Create the UI elements

// I assume you have a question label, answer text field and drop down button
// Set up should be adjusted in case of autolayout
let questionLabel = UILabel(frame: CGRect(x: 10, y: 100, width: 250, height: 20))
let answerTextField = UITextField(frame: CGRect(x: 10, y: 130, width: 250, height: 50))
let dropDownButton = UIButton()

2. Regular setup of the label and the text view first

// Just regular set up
private func configureQuestionLabel()
{
    questionLabel.textColor = .white
    view.addSubview(questionLabel)
}

// Just regular set up
private func configureAnswerTextField()
{
    let placeholderAttributes = [NSAttributedString.Key.foregroundColor :
                                    UIColor.lightGray]
    
    answerTextField.backgroundColor = .white
    answerTextField.attributedPlaceholder = NSAttributedString(string: "Tap to select a question",
                                                               attributes: placeholderAttributes)
    answerTextField.textColor = .black
    
    view.addSubview(answerTextField)
}

3. Add the button to the text view

// Here we have something interesting
private func configureDropDownButton()
{
    // Regular set up
    dropDownButton.setImage(UIImage(systemName: "chevron.down"), for: .normal)
    dropDownButton.showsMenuAsPrimaryAction = true
    
    // 1. Create the menu options
    // 2. When an option is selected update the question label
    // 3. When an option is selected, update the button frame
    // Update button width function explained later
    dropDownButton.menu = UIMenu(title: "Select a question", children: [
        
        UIAction(title: "Favorite movie") { [weak self] action in
            self?.questionLabel.text = action.title
            self?.answerTextField.placeholder = "Add your answer"
            self?.answerTextField.text = ""
            self?.updateButtonWidthIfRequired()
        },
        
        UIAction(title: "Car make") { [weak self] action in
            self?.questionLabel.text = action.title
            self?.answerTextField.placeholder = "Add your answer"
            self?.answerTextField.text = ""
            self?.updateButtonWidthIfRequired()
        },
    ])
    
    // I right align the content and set some insets to get some padding from
    // the right of the text field
    dropDownButton.contentHorizontalAlignment = .right
    dropDownButton.contentEdgeInsets = UIEdgeInsets(top: 0.0,
                                                    left: 0.0,
                                                    bottom: 0.0,
                                                    right: 20.0)
    
    // The button initially will stretch across the whole text field hence we
    // right aligned the content above
    dropDownButton.frame = answerTextField.bounds
    
    answerTextField.addSubview(dropDownButton)
}

// Update the button width if a menu option was selected or not
func updateButtonWidthIfRequired()
{
    // Button takes the text field's width if the question isn't selected
    if let questionText = questionLabel.text, questionText.isEmpty
    {
        dropDownButton.frame = answerTextField.bounds
        return
    }
    
    // Reduce button width to right edge as a question was selected
    dropDownButton.frame = CGRect(x: answerTextField.frame.width - 50.0,
                                  y: answerTextField.bounds.origin.y,
                                  width: 50,
                                  height: answerTextField.frame.height)
}

4. End Result

Start with a similar view to yours

Custom UITextField with dropdown Menu

Then I tap in the middle of the text field

UITextField with drop down menu

It displays the menu as intended

Show UIMenu UIContextMenuInteraction from UITextField UIButton

After selecting an option, the question shows in the label and the placeholder updates

UIMenu UIContextMenuInteraction from UIButton

Now I can start typing my answer using the text field and the button is only active on the right side since it was resized

UITextField with UIMenu from UIButton on UIContextMenuInteraction

And the button is still active

UIMenu from UIButton

Final thoughts

  1. Could be better to put this into a UIView / UITextField subclass
  2. This was an example using frames with random values, adjustments need to made for autolayout

Edits from OP (too long for a comment):

  • I had tried setting contentEdgeInsets so the button was way over to the left but it covered up the placeholder text.
  • Your idea of simply adding the button as a subview of the text field was the key, but...
  • After selecting a question and resizing the button, if the text field was the first responder, tapping the button had no effect. The button was in the view hierarchy but the text field got the tap.
  • So, when a question is selected, I remove the button from its superview (the textfield) and add it to the textField's rightView. Then it would accept a tap even if the textField was the first responder.
  • And, as you suspected, I had to pin the button to the textField with constraints.