iosswiftuibuttonuiaction

In iOS 14 how do you tap a button programmatically?


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.


Solution

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