This article is aimed at creating a simple dropdown selector that is reusable using SwiftUI. Below is the result I’m looking forward to:
As you may know, SwiftUI is pretty cool when it comes to creating custom views easily. I would harness this capability to actually create the dropdown as shown above. So let’s get to it.
I have a SwiftUI view in a single file, DropdownSelector.swift
where all the code would go in. You can decide to split some of the components within the file into multiple files, as you wish.
import SwiftUI struct DropdownSelector: View { var body: some View { Text("Hello, World!") } } struct DropdownSelector_Previews: PreviewProvider { static var previews: some View { DropdownSelector() } }
All I have right now is an empty view with just a Text. I would create a simple struct DropdownOption
, representing a single option that a user can select. You can decide to keep the option as a String but to be safe when it comes to comparing selections and avoiding having similar values. The DropdownOption
struct contains a key
and a value
.
struct DropdownOption: Hashable { let key: String let value: String public static func == (lhs: DropdownOption, rhs: DropdownOption) -> Bool { return lhs.key == rhs.key } }
I have the struct conform to Hashable
, knowing I would at some point later on make use of a ForEach
to loop through the available options. I could use Identifiable
protocol as well, but that would require that the struct has an id
, which this doesn’t have right now.
Next is to add contents to the DropdownSelector
view itself.
struct DropdownSelector: View { @State private var shouldShowDropdown = false @State private var selectedOption: DropdownOption? = nil var placeholder: String var options: [DropdownOption] var onOptionSelected: ((_ option: DropdownOption) -> Void)? private let buttonHeight: CGFloat = 45 var body: some View { Button(action: { self.shouldShowDropdown.toggle() }) { HStack { Text(selectedOption == nil ? placeholder : selectedOption!.value) .font(.system(size: 14)) .foregroundColor(selectedOption == nil ? Color.gray: Color.black) Spacer() Image(systemName: self.shouldShowDropdown ? "arrowtriangle.up.fill" : "arrowtriangle.down.fill") .resizable() .frame(width: 9, height: 5) .font(Font.system(size: 9, weight: .medium)) .foregroundColor(Color.black) } } .padding(.horizontal) .cornerRadius(5) .frame(width: .infinity, height: self.buttonHeight) .overlay( RoundedRectangle(cornerRadius: 5) .stroke(Color.gray, lineWidth: 1) ) .overlay( VStack { if self.shouldShowDropdown { Spacer(minLength: buttonHeight + 10) Dropdown(options: self.options, onSelect: { option in shouldShowDropdown = false selectedOption = option self.onOptionSelected?(option) }) } }, alignment: .topLeading ) .background( RoundedRectangle(cornerRadius: 5).fill(Color.white) ) } }
Let me explain all that is being done above:
- I have a @State variable
shouldShowDropdown
, which is used to show or hide the dropdown when the DropdownSelector is tapped. selectedOption
@State variable is to be used to keep track of user selection and replace theplaceholder
.- There are three required params to be passed in when using DropdownSelector,
placeholder
which is the greyed out text to display before the user selects an option,options
which is a list of theDropdownOption
we created above,onOptionSelected
which is a closure called when the user makes a selection. - A couple of UI implementations to create the look that is intended, including an overlay where the
Dropdown
(actual selection list) is displayed or hid. ThisDropdown
would be created in the next step.
The next step is to create the Dropdown
view that displays the list of options.
struct Dropdown: View { var options: [DropdownOption] var onOptionSelected: ((_ option: DropdownOption) -> Void)? var body: some View { ScrollView { VStack(alignment: .leading, spacing: 0) { ForEach(self.options, id: \.self) { option in DropdownRow(option: option, onOptionSelected: self.onOptionSelected) } } } .frame(height: 100) .padding(.vertical, 5) .background(Color.white) .cornerRadius(5) .overlay( RoundedRectangle(cornerRadius: 5) .stroke(Color.gray, lineWidth: 1) ) } }
Above, is a simple view that contains a stack of DropdownRow
(single option row item) by iterating the dropdown options
, all wrapped in a ScrollView. This view takes two parameters, a list of dropdown options
and a closure, onOptionSelected
to be triggered when an option is selected.
The DropdownRow
would be created below:
struct DropdownRow: View { var option: DropdownOption var onOptionSelected: ((_ option: DropdownOption) -> Void)? var body: some View { Button(action: { if let onOptionSelected = self.onOptionSelected { onOptionSelected(self.option) } }) { HStack { Text(self.option.value) .font(.system(size: 14)) .foregroundColor(Color.black) Spacer() } } .padding(.horizontal, 16) .padding(.vertical, 5) } }
This is basically a text button that calls the onOptionSelected
on tap. It takes a single dropdown option
as parameter including the closure.
Looks like right now, there is a working copy of the DropdownSelector, all that is left is to display it in the Canvas by making use of the PreviewProvider.
struct DropdownSelector_Previews: PreviewProvider { static var uniqueKey: String { UUID().uuidString } static let options: [DropdownOption] = [ DropdownOption(key: uniqueKey, value: "Sunday"), DropdownOption(key: uniqueKey, value: "Monday"), DropdownOption(key: uniqueKey, value: "Tuesday"), DropdownOption(key: uniqueKey, value: "Wednesday"), DropdownOption(key: uniqueKey, value: "Thursday"), DropdownOption(key: uniqueKey, value: "Friday"), DropdownOption(key: uniqueKey, value: "Saturday") ] static var previews: some View { Group { DropdownSelector( placeholder: "Day of the week", options: options, onOptionSelected: { option in print(option) }) .padding(.horizontal) } } }
Above, I have created a list of options that are passed to the DropdownSelector
. I utilised the UUID to generate a unique key for each of the options. Take a look at the result below.
This is good. However, if at some point, I had to provide just one option, the dropdown would end up looking like below:
A fixed height (100pt) is set for the Dropdown, this is not a good experience we expect. So how can this be made better?
struct Dropdown: View { var options: [DropdownOption] var onOptionSelected: ((_ option: DropdownOption) -> Void)? var body: some View { ScrollView { VStack(alignment: .leading, spacing: 0) { ForEach(self.options, id: \.self) { option in DropdownRow(option: option, onOptionSelected: self.onOptionSelected) } } } .frame(minHeight: CGFloat(options.count) * 30, maxHeight: 250) .padding(.vertical, 5) .background(Color.white) .cornerRadius(5) .overlay( RoundedRectangle(cornerRadius: 5) .stroke(Color.gray, lineWidth: 1) ) } }
Notice the change in the frame? Here, I set a minimum height that is equal to the number of options multiplied by a fixed height (30) and also a maximum height (250), which ensures the dropdown doesn’t grow too much but instead allows you to scroll. Take a look at the results below, for both single and multiple options:
There you go! A simple and reusable dropdown selector that can further be customised. You can find the full source code here.
Cheers!
Really nice and easy to understand
Good job, well-done..
Hi there,
Could you guide me of as to how I can update the options of a specific dropdown (I am using four and need to update one of the dropdown options when a selection is made in the other dropdowns)
Thank You.
Hello Aditya, since the dropdown options are
State
variables, changes to them automatically reflect in the corresponding Dropdowns using them. So all you need to do is to update the particular dropdown options array (appending to the array).Example:
dropdownOptions1.append(DropdownOption(key: "Key goes here", val: "Value goes here"))
I’m new to swift, but my rec is instead of making DropdownOption a struct, make it a generic so we don’t have too many object orienty things going on and have the compiler infer the type of the option in the array.
Hey R. This sounds interesting. Do you mind showing some examples?
This does not seem to work if used within a SwiftUI Form – do you have any recommendations?
Hey Samuel. I haven’t tried it with SwiftUI Form, so I don’t have a recommendation right now. I would do some findings and get back to you.
Hi, I’m still relatively pretty brand new to swift. How do we define a variable outside the struct that can use the value of the selected option? For example if I click Sunday, I can have a variable that receives that value so I can use it on another file.
Hi David, since the
DropdownSelector
is a reusable view, I wouldn’t suggest keeping data coming from it ‘within’ the file. There is a selectedOption State variable that keeps track of selected option and can be listened to, as well as onOptionSelected callback.Hi Emmanuel, still learning Swift here… can you provide an example on how to implement the onOptionSelected callback? I want to show views below the dropdown conditionally based on the selected option.
Appreciate any guidance. Thanks
Hi Kim,
You can check out the view preview area for a sample code like this:
Group {
DropdownSelector(
placeholder: "Day of the week",
options: options,
onOptionSelected: { option in
print(option)
})
.padding(.horizontal)
}
You’d notice I’m only printing the selected option here, you can also at that point, show views conditionally from there.
How do you programmatically select an option after the dropdown has been populated
Hi timtim, I recently made an update to the drop-down component. I’m yet to update it here but how that works is to add extra parameter to the drop-down component that allows you to pass default value or index.
Great work! Say I had other views right below this dropdown and when I expanded the dropdown it would appear above the other views and the dropdown box would cover the other views. How could I do that? Right now, It seems the other views will appear as if the dropdown box had a clear background (Which it doesn’t)
Hi Jake, you can ensure the drop-down component (containing the list of items) has a white background.
Hi
There is an error in DropdownSelector when you call the Dropdown the second parameter is onOptionSelected and not onSelect
Hi Emmanuel
Thank you
There is an error in DropdownSelector when you call the Dropdown the second parameter is onOptionSelected and not onSelect
Hi, Emmanuel Great work!
What about multiple dropdown boxes just like form, with only one dropdown option opened each time, how do we handle this?