Custom UI Master Class: Dropdown Menu with TextField

Tim Beals 🎸
Swift2Go
Published in
9 min readNov 26, 2018

--

Fork the repository for this demo here

Use Case

Dropdown menus should be employed sparingly and reserved for situations when you want your user to select from a fairly limited range of options. Visually the screen real estate that the UI occupies is comparable to a textfield, so you can imagine using it in a ViewController that is busy with several other UI elements. Its often advisable to push to a second ViewController providing options in its own TableView instead of implementing a dropdown menu. However, in cases where this kind of segue interrupts the user flow or where the options are few enough that a segue seems excessive, the dropdown menu can be an elegant solution. The component we will build today includes a textfield so that the user can enter their own option if the presented options don’t cover their case.

DropdownTextField: Overview

The logic involved with our dropdown is not so complex, but there is a lot of interplay between the different views, some of which are animated or hidden at different times. To get an appreciation of what we are building and how the parts work together, let’s examine the diagram below:

TapView: At the top of the view hierarchy is a transparent view. It receives user taps through a gesture recognizer which triggers the dropdown animation.

TextField: Directly beneath the TapView is a TextField that displays the selected option or allows the user to provide their own input when ‘Other’ is selected from the menu.

AnimationView: To get the dropdown effect that we are looking for, animations are performed on the AnimationView adjusting its frame to give the appearance of going up and down.

TableView: Directly over the AnimationView, the TableView displays the menu options, and provides option selection through a delegate method.

Let’s begin by adding and arranging our views as described here.

Step 1: Create Subview Hierarchy

As we have already seen, there is a lot of initial setup of views that needs to happen. To assist with the process, let’s take a quick look at some helpful extensions. First we have a Theme enum that we put in an extension of UIImage. We only have one case for this demo, but you can see how easy it is to add lots of images as different cases, and then enter the image name in the name property switch. With this setup, we access our triangle image using UIImage.Theme.triangle.image and we don’t need to worry about any typos along the way.

This next snippet is a convenience method added to an extension of UIView, and allows for very simple addition of layout constraints in the case of pinning a view’s edges. All that is required is to pass the four anchors as input arguments, set translatesAutoresizingMaskIntoConstraints to false and then add and activate the constraints.

Now we can put these to work. Below, we create a custom class DropDownTextField subclassing UIView (line 1). Among the public properties (lines 4–7) we provide default values for three colours and a font to make the appearance of our component consistent. These are exposed so that the colours can be changed if desired.

Next we declare our private properties (lines 10–12) including an array of options that are displayed in the dropdown menu, an initialHeight which we set in an initializer and reference several times setting up subviews, and a rowHeight for our tableView. Our subview declarations follow with some initial setup (lines 15–45). We won’t go into much depth here, but notice that we set the triangleIndicator image using the Theme enum discussed earlier. We also set the rendering mode withRenderingMode(.alwaysTemplate) (line 19) so that we can assign its tintColor later . In our tableView we register a custom cell class DropDownCell which we are yet to create, and then comment it out (line 29). We also assign the separatorColor and adjust the separatorInset so that our separator line goes nicely across the entire width of our view. In order to access our previously defined colour properties in both the textField and tableView, we use the lazy keyword (lines 47 & 59).

Now we can move onto our initializer. Determining the final frame and setup of the subviews depends entirely on how many options are provided, so we create a custom initializer with input parameters for frame, title and options (lines 47–53). In order to ensure this is the only initializer available, we also mark init(frame:) as private (lines 55–58).

In our custom initializer we assign options to its namesake property and the title to the textField.text property. Now things get interesting. We expect the frame argument to be the same as if we were adding a textfield, which means that we need to calculate the height of our tableView such that it accommodates all of our options and adds its height to that of our textfield. We do this in the calculateHeight() method (lines 71–76).

Finally we setup our subviews using setupViews() (lines 78-86). This method calls several smaller private methods that are responsible for first removing the subviews from the superview, and then adding them one at a time setting their constraints to achieve the desired look and hierarchy. Again, we won’t go into much detail, but notice that we set the tintColor property of our triangleIndicator (line 109). In our tapView we add a tap gesture recognizer (lines 134–135) and assign the action to the selector method animateMenu() (lines 64–66). We use the UIView extension method constraintsPinTo(leading: trailing: top: bottom:) described above to set up both our tapView and tableView. Unlike the other subviews, the animationView is setup with a frame as it was my personal preference to animate frames over constraints in this case (line 152). You will also notice that we have commented out assignment of self for UITableViewDelegate, UITableViewDatasource, UITextFieldDelegate, which we will address in later steps. Finally, we hide tableView and animationView (lines 147 & 155) but comment those lines out in order to check they have been added to the view hierarchy correctly.

Going over to our ViewController, we can now initialize our dropDownTextField using the custom initializer described in the previous snippet. You will also notice that there is a delegate assignment which has been commented out, and we will address that in the next step (line 2).

If we build and run, this is hopefully what we should see:

Step 2: Create and Assign Delegate

The good news is that the subviews appear to be where we want them, and our big chunk of setup code has now been taken care of. Our goal now is to create a delegate so that our ViewController can receive information from the dropDown. The first file (below) shows the creation of the DropDownTextFieldDelegate protocol, inside which we find two methods. The first, menuDidAnimate(up:) will let the ViewController know every time the menu is about to animate, and the direction of the animation. This is important to know because if the ViewController has a busy view hierarchy there will most likely need to be some shuffling of its subviews depending on whether the menu is down or up. The second method optionSelected(option:) is pretty self explanatory as the ViewController most likely wants to receive the selected option.

With the protocol defined, we return to our DropDownTextField class and create a new property of our delegate type (line 8). Looking at the second file, we adopt the protocol in our ViewController (line 2) and add print statements to our two methods for demo purposes. With this completed, we can uncomment the delegate assignment in the method addDropDown() from our previous snippet.

Step 3: Add Options to TableView

Our next step is to get our options to appear in the tableView. First we need our custom DropDownCell subclassing UITableViewCell. There’s very little required here. We add lightColor and cellFont properties to ensure that the cell is styled consistently with the colours and font of our DropDownTextField. We will assign them soon, but for now provide default values (lines 3–4). We require only one method configureCell(with title:), in which we pass the title string and do some basic appearance configuration.

Now we can uncomment cell registration and the delegate and datasource assignment from the previous snippet and then add the following methods in an extension:

Notice that we are using the options property as our datasource. We determine the number of rows by getting the options count and adding one, in order to accommodate our final option “Other” which we will display at the bottom of the tableView (line 4). In cellForRowAt, we dequeue our custom cell, setting the lightColor and cellFont properties (lines 9–10). Then we get either the string option from the datasource or assign “Other” if it is our final row (line 11) before passing that string into our configureCell(with: ) method (line 12).

Once again, we build and run. Hopefully we should see this:

Step 4: Add Animation and Option Selection

With our options displaying correctly in our tableview, we can now turn our attention to animating the appearance of our menu and handling option selection. Our animationView and tableView are both hidden upon initialization, so we can uncomment the isHidden lines from Step 1. We next implement the method menuAnimate(up:) (lines 9–24). To get our desired effect, we set the frame and isHidden properties of both the animationView and tableView depending on whether the menu is animating down or up. We also update the current state of our UI, stored in a new boolean isDroppedDown (line 4), and notify our delegate with the direction of the animation (line 23). This method can be called from animateMenu(), which you will remember is the selector method called by our tapView.

In order to address selection of an option, we return to our tableView delegate methods, and implement logic in didSelectRow (lines 29–38). Notice that if the user selects ‘Other’ in the last row nothing happens yet (lines 34–36), but selection of a provided option will update the textField text, notify the delegate, and animate the menu back up.

Build and run. Hopefully you will be able to tap on the UI causing the menu drop down, and select an option from the menu resulting in the up animation and update of the textField text. Also look for the print statements in the console from the delegate methods notifying when animation and option selection occurs.

Step 5: Add TextField Logic

At the moment, if the user taps on the last cell in the menu marked “Other”, nothing happens. Let’s fix that. Upon selection, we want to animate the menu up and handle the user typing their own option into the textField. First we need to uncomment the assignment of self as the textfield delegate which we saw in step one. Then we implement otherChosen() (lines 14–18) which animates the menu up, clears the textField and assigns it as the firstResponder. Don’t forget to uncomment the method call in the previous snippet! We also add logic to two delegate methods. In textFieldDidEndEditing (lines 3–8) we get the text from the textField using optional binding, capitalize it, set it back into our textField and pass it as a selected option to our delegate. Depending on your own use case, you may want to add further validation that the option type is legitimate. We want to have a return button tap as entry of the user input, so in textFieldShouldReturn (lines 10–13) we simply resignFirstResponder, which has the effect of ending editing and animating the software keyboard away.

For the last time, build and run the application.

Wrap Up

And that’s it! Our goal for this article was create a dropdown menu with the additional ability to take user input. The implementation required custom initialization, calculating the required frame to accommodate the desired number of options. We also configured and arranged a lot of different types of subviews which we achieved entirely in code. We worked with delegates for both the TableView and TextField, and then created our own delegate in order to communicate with the ViewController. Best of all, we now have a very flexible component that we can use in any project we like, and feel comfortable customizing its appearance and performance to meet our needs.

I hope that you got a lot out of the article and as usual, thanks for reading!

If you enjoyed this article, why not check out one of my previous UI Masterclasses: Infinite Paging ScrollView, & Autocomplete TextField

--

--