Building a Custom MultiSelect Picker in SwiftUI

Jimoh YusuphJimoh Yusuph
5 min read

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 need a model to represent each option in the picker. We’ll use a class FormOption , it has an isSelected property to manage the selection state. you can create your own class or struct to suit your project and handle the logic of selection in a way that best fits your needs.

class FormOption: Hashable, Identifiable, Equatable {
    var label: String
    var value: String?
    var uid: String?
    var id: String = UUID().uuidString
    var isSelected: Bool = false

    static func == (lhs: FormOption, rhs: FormOption) -> Bool {
        return lhs.id == rhs.id
    }

    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }

    init(label: String, value: String? = nil, uid: String? = nil, isSelected: Bool = false) {
        self.label = label
        self.value = value
        self.uid = uid
        self.isSelected = isSelected
    }
}

Why a class? I use a class instead of a struct because a class is a reference type. This ensures that the isSelected property retains its state across different views. As a result, it becomes easier to filter the selected items from the list of options passed into the picker, which I will demonstrate later in the tutorial

Step 2: 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.

@available(iOS 13.0, *)
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 store all the options in a list and display them using a SwiftUI List. When the user taps the button to select multiple options, the options are shown in the list, and each can be toggled on or off.

The selection logic is managed by the toggleSelection function. When a user selects an option, the option's isSelected property is toggled to true and toggled back to false if deselected.

An optional selectedOptions set is used to keep track of the selected item, primarily used to display the selected items on the UI.

struct OptionsSelectionSheet: View {
    var options: [FormOption]
    var title: String
    @Binding var selectedOptions: Set<FormOption>
    @Binding var sheetIsOpen: Bool

    var body: some View {
        VStack {
            HStack {
                Text(title)
                    .foregroundColor(.secondary)
                Spacer()

                Button(action: {
                    sheetIsOpen = false
                }) {
                    Text("Done")
                }
            }
            .padding()

            List(options, id: \.self) { option in
                MultipleSelectionRow(option: option, isSelected: option.isSelected) {
                    toggleSelection(for: option)
                }
            }
            .listStyle(.plain)
        }
    }

    private func toggleSelection(for option: FormOption) {
        option.isSelected.toggle()

        if option.isSelected {
            selectedOptions.insert(option)
        } else {
            selectedOptions.remove(option)
        }
    }
}

FYI: You can modify the logic to manage the selected state using only the selectedOptions array, which will eliminate the need for the isSelected property. I will prefer to use both in this tutorial.

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 4: Building the Multi Picker View (The Moment We've Been Waiting For)

Next, we create the picker view that will display the list of options, and we’ll modify it to look like a default SwiftUI PickerView. When users tap on the displayed selection, it present the options selection sheet.

public struct MultiOptionListView: View {
    public var title: String
    @Binding public var options: [FormOption]
    @State private var selectedOptions: Set<FormOption> = []
    @State private var showOptionsSheet = false

    public init(
            title: String,
            options: Binding<[FormOption]>
        ) {
            self.title = title
            self._options = options
        }

    public var body: some View {
        VStack(alignment: .leading) {
            Button(action: {
                showOptionsSheet.toggle() // Open the sheet
            }) {
                VStack {
                    HStack {
                        // Display selected labels or the title if nothing is selected
                        Text(selectedLabels.isEmpty ? title : selectedLabels.joined(separator: ", "))
                            .foregroundColor(selectedLabels.isEmpty ? .gray : .primary)
                        Spacer()
                        Image(systemName: "chevron.right.circle")
                            .foregroundColor(.accentColor)
                    }
                }
                .frame(alignment: .leading)
                .padding()
                .background(Color.gray.opacity(0.1))
                .cornerRadius(10)
            }
            .sheet(isPresented: $showOptionsSheet) {
                OptionsSelectionSheet(
                    options: $options, 
                    title: title,
                    selectedOptions: $selectedOptions,
                    sheetIsOpen: $showOptionsSheet
                )
                .presentationDetents([.medium])
            }
        }
        .onAppear{
            // This ensures that items initially marked as selected
            // are automatically pre-selected the first time the user
            // opens the picker. Remove this line if your use case does not
            // require pre-selection.    
            selectedOptions = Set(options.filter { $0.isSelected })
        }
    }

    var selectedLabels: [String] {
        return options.filter { $0.isSelected }.map { $0.label ?? "" }
    }
}

Finally, here's how we can use the picker in our app:

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 {
        VStack {
            MultiOptionListView(title: "Skills",options: $options)
        }

    }
}

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.0")

Feel free to tweak the FormOption model or any part of the code to suit your app's needs.

Happy Coding 💻

1
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 and Full Stack Engineer 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.