Building a Custom MultiSelect Picker in SwiftUI

SwiftUI has revolutionized how we build user interfaces on Apple platforms, making it easier and faster to create modern, responsive apps. While SwiftUI provides excellent built-in controls like Picker
for single selections, there isn’t a native way for multiple selections.
In this tutorial, we will walk through how to build a custom MultiSelectPicker in SwiftUI. the component allows users to select multiple options from a list, we'll also cover how to handle pre-selection of items.
Creating a MultiSelect Picker in SwiftUI
Let’s break down the process step by step.
Step 1: Define the FormOption
Model
We’ll use a FormOption
struct to represent each option’s data. Some implementations may prefer a class and introduce isSelected
property to manage selection state, as classes are reference types and preserve state across views. You can either conform your own model to the FormOption
structure or create a custom class or struct that best fits your project’s architecture and selection logic.
struct FormOption: Hashable, Identifiable {
public var id: String
public var label: String
public var value: String
init(id: String = UUID().uuidString, label: String, value: String) {
self.id = id
self.label = label
self.value = value
}
}
Why a struct? A struct is value-safe, easy to test, avoids unintended mutations, and aligns well with SwiftUI’s emphasis on immutability. It helps prevent side effects and ensures more predictable behavior in your views.
Note: Make sure each option has a unique id
, as it is used internally to manage selection for each item.
Step 2: Manage Logic with a ViewModel
We use a MultiSelectPickerViewModel
to manage all logic in one place, which helps keep the UI clean and makes the logic testable, reusable, and easy to maintain.
@MainActor
class MultiSelectPickerViewModel: ObservableObject {
@Published var allOptions: [FormOption]
@Published var selectedOptions: Set<FormOption>
@Published var selectedIDs: Set<String>
init(options: [FormOption], preSelected: [FormOption] = []) {
self.allOptions = options
//Handling preselection
let preSelectedIDs = Set(preSelected.map { $0.id })
let selected = options.filter { preSelectedIDs.contains($0.id) }
self.selectedOptions = Set(selected)
self.selectedIDs = preSelectedIDs
}
func toggleSelection(_ option: FormOption) {
if selectedOptions.contains(option) {
selectedOptions.remove(option)
selectedIDs.remove(option.id)
} else {
selectedOptions.insert(option)
selectedIDs.insert(option.id)
}
}
}
Properties
@Published var allOptions
:
This holds the complete list of selectable items passed into the picker.@Published var selectedOptions
:
We use theselectedOptions
set to keep track of what’s currently selected.
This is also useful for returning selected values and displaying selected labels in the UI
The selection logic is handled by the toggleSelection
function. It updates which options are selected.
Preselection Logic
if you want to support preselected options, it's handled during initialization.
The IDs of the preSelected
items are compared with those in allOptions
to set up the initial selection.
let preSelectedIDs = Set(preSelected.map { $0.id })
let selected = options.filter { preSelectedIDs.contains($0.id) }
Why Use selectedIDs
?
Though selectedOptions
holds the selected objects, we also maintain a separate selectedIDs: Set<String>
for these reasons:
Fast lookups –
.contains(id)
is much quicker than comparing entire objects.Efficient rendering – Ideal for use in
List
and SwiftUI views where identity is based onid
.Better scalability – Keeps the UI logic simple and performs well, especially with large lists.
FYI: I use a set instead of an array because it automatically handles uniqueness and provides faster lookups and removals with O(1) time complexity.
Step 3: Create the Option Item Row
Each option in the picker are displayed in a row. We create a view that will show each option as a Text
and a checkmark if it’s selected.
struct MultipleSelectionRow: View {
var option: FormOption
var isSelected: Bool
var onTap: () -> Void
var body: some View {
Button(action: onTap) {
HStack {
Text(option.label)
Spacer()
if isSelected {
Image(systemName: "checkmark")
.foregroundColor(.accentColor)
}
}
}
}
}
Step 3: Displaying and Handling Selection
We display all available options using a SwiftUI List
. When the user taps to open the picker, the list appears, allowing each item to be selected or deselected. As mentioned earlier, the selection logic is handled inside the ViewModel
, not directly in the view, which helps to keep the UI clean.
I've also added helpful features like search and clear selection to optimize performance for large datasets and improve the overall user experience. However, I won’t go into detail on those features in this article.
struct OptionsSelectionSheet: View {
var allOptions: [FormOption]
var title: String
@Binding var selectedIDs: Set<String>
var onToggle: (FormOption) -> Void
var onDone: () -> Void
var body: some View {
List {
ForEach(allOptions, id: \.id) { option in
MultipleSelectionRow(
option: option,
isSelected: selectedIDs.contains(option.id),
onTap: { onToggle(option) }
)
}
}
.listStyle(.plain)
.searchable(text: $searchText, prompt: "Search \(title.lowercased())")
.navigationTitle(title)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
if !selectedIDs.isEmpty {
Button("Clear", action: onClear)
.foregroundColor(.red)
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button("Done", action: onDone)
}
}
}
}
}
Step 4: Building the Multi Picker View (The Moment We've Been Waiting For)
Next, we create the picker view that displays the list of options and style it to resemble the native SwiftUI Picker
. When users tap on the selection field, it present the options selection sheet we created earlier.
I'm an advocate for accessibility, so I’ve made sure the component includes appropriate accessibility labels and traits to support VoiceOver users.
struct MultiSelectPicker: View {
var title: String
@Binding public var options: [FormOption]
var preSelected: [FormOption]
var onDone: ((Set<FormOption>) -> Void)? = nil
@StateObject private var viewModel: MultiSelectPickerViewModel
@State private var showOptionsSheet = false
init(
title: String,
options: Binding<[FormOption]>,
preSelected: [FormOption] = [],
onDone: ((Set<FormOption>) -> Void)? = nil
) {
self.title = title
self._options = options
self.preSelected = preSelected
self.onDone = onDone
_viewModel = StateObject(wrappedValue: MultiSelectPickerViewModel(
options: options.wrappedValue,
preSelected: preSelected
))
}
var body: some View {
VStack(alignment: .leading) {
Button {
showOptionsSheet.toggle()
} label: {
HStack {
Text(displayedLabelText)
.accessibilityLabel(accessibilityLabelText)
Spacer()
Image(systemName: "chevron.down")
}
.padding()
.background(Color.gray.opacity(0.2))
.cornerRadius(10)
}
.accessibilityElement(children: .combine)
.sheet(isPresented: $showOptionsSheet) {
OptionsSelectionSheet(
allOptions: viewModel.filteredOptions,
title: title,
selectedIDs: $viewModel.selectedIDs,
searchText: $viewModel.searchText,
onToggle: { viewModel.toggleSelection($0) },
onClear: viewModel.clearSelection,
onDone: {
showOptionsSheet = false
options = viewModel.allOptions
onDone?(viewModel.selectedOptions)
}
)
}
}
}
private var displayedLabelText: String {
viewModel.selectedOptions.isEmpty
? title
: viewModel.selectedOptions.map { $0.label }.joined(separator: ", ")
}
private var accessibilityLabelText: String {
viewModel.selectedOptions.isEmpty
? title
: "Selected: \(viewModel.selectedOptions.map { $0.label }.joined(separator: ", "))"
}
}
Finally, here's how we can use the picker in our app:
import SwiftUI
import MultiSelectPicker
struct ContentView: View {
@State var options: [FormOption] = [
FormOption(label: "Reading", value: "reading"),
FormOption(label: "Traveling", value: "traveling"),
FormOption(label: "Cooking", value: "cooking"),
FormOption(label: "Gaming", value: "gaming")
]
var body: some View {
MultiSelectPicker(title: "Hobbies", options: $options,
onDone: { selected in
print("Selected:", selected)
}
)
.padding()
}
}
Wrapping Up
We've successfully built a custom multi-option picker in SwiftUI that allows users to select multiple options with full control of the design and functionality.
I’ve also created a Swift package that you can integrate directly into your project, or simply copy the code and use in your app:
.package(url: "https://github.com/yusuphjoluwasen/MultiSelectPicker.git", from: "1.0.3")
Feel free to tweak the FormOption
model or any part of the code to suit your app's needs.
Happy Coding 💻
Subscribe to my newsletter
Read articles from Jimoh Yusuph directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Jimoh Yusuph
Jimoh Yusuph
I'm an iOS with a passion for creating secured high-performance applications. I enjoy reading and exploring ways to enhance app functionalities and efficiency. I have strong practical experience developing applications in Swift, Kotlin, Javascript, Python, and Flutter. Having led teams in the past, I greatly value collaboration and teamwork, and I take pleasure in sharing my knowledge with others.