Adding interactivity to your SwiftUI apps often involves knowing how to listen and respond to user input. One of the most common UI elements in SwiftUI is the Picker, used for enabling users to select a single value from a list. But while Pickers are intuitive and easy to use, SwiftUI doesn’t offer an explicit way to bind action listeners like in UIKit. Still, with a few smart tricks, you can effectively add actions to Picker selections and create a more dynamic user interface.
TL;DR
In SwiftUI, you can’t directly attach actions to a Picker. However, you can monitor changes using bindings, and especially the .onChange modifier introduced in iOS 14. You can also use property observers like didSet or even leverage Combine for more advanced reactions. With these techniques, you can easily perform logic whenever a Picker’s value changes and make your SwiftUI apps more responsive and intelligent.
Understanding the SwiftUI Picker
The Picker in SwiftUI is a powerful UI element used for selection. It binds to a state variable, and its selection automatically updates the bound value. Here’s a basic example:
struct ContentView: View {
@State private var selectedFruit = "Apple"
let fruits = ["Apple", "Banana", "Cherry"]
var body: some View {
Picker("Choose a fruit", selection: $selectedFruit) {
ForEach(fruits, id: \.self) {
Text($0)
}
}
.pickerStyle(SegmentedPickerStyle())
}
}
The Picker works beautifully, but if you want to perform an action every time the selection changes, you’ll need to add a little more logic.
Using .onChange to Trigger Actions
Introduced in iOS 14+, the .onChange modifier provides a clean and SwiftUI-native way to listen for changes in a Picker’s bound state:
Picker("Choose a fruit", selection: $selectedFruit) {
ForEach(fruits, id: \.self) {
Text($0)
}
}
.onChange(of: selectedFruit) { newValue in
print("User selected: \(newValue)")
}
This approach is arguably the best for newer projects, as it directly hooks into SwiftUI’s reactive data flow. The closure receives the new value selected by the user and allows you to perform any required updates.
Real-world Scenario: Update Another View
Let’s say you want to update a label below the Picker to reflect the selected fruit:
struct ContentView: View {
@State private var selectedFruit = "Apple"
@State private var message = ""
let fruits = ["Apple", "Banana", "Cherry"]
var body: some View {
VStack {
Picker("Choose a fruit", selection: $selectedFruit) {
ForEach(fruits, id: \.self) {
Text($0)
}
}
.onChange(of: selectedFruit) { newValue in
message = "You picked \(newValue)!"
}
Text(message)
.padding()
}
}
}
This makes your app immediately responsive, offering feedback as the user interacts with the interface.
Alternative: Observing Property with didSet
If you’re targeting earlier iOS versions or simply want to keep the logic outside of modifiers, you can use a model with a didSet property observer.
class SelectionModel: ObservableObject {
@Published var selectedFruit = "Apple" {
didSet {
print("New fruit selected: \(selectedFruit)")
}
}
}
And in your View:
struct ContentView: View {
@StateObject private var model = SelectionModel()
let fruits = ["Apple", "Banana", "Cherry"]
var body: some View {
Picker("Choose a fruit", selection: $model.selectedFruit) {
ForEach(fruits, id: \.self) {
Text($0)
}
}
.pickerStyle(SegmentedPickerStyle())
}
}
The didSet approach provides good separation of concerns and works across different iOS versions. However, it’s less controllable than .onChange, especially if you’re considering asynchronous operations.
Using Combine to React to Changes
For developers using more complex data models or requiring side effects like API calls, integrating Combine lets you subscribe to changes reactively.
class FruitSelection: ObservableObject {
@Published var selectedFruit: String = "Apple"
private var cancellables = Set<AnyCancellable>()
init() {
$selectedFruit
.sink { newValue in
print("Select changed through Combine: \(newValue)")
}
.store(in: &cancellables)
}
}
This lets you build pipelines of side effects, transformations, and animations tied to Picker selection changes — all cleanly managed in reactive code.
UX Tip: Animate on Selection Change
Sometimes, you may want to use Picker selections to trigger simple animations or transitions. Using withAnimation inside your .onChange block can create delightful user experiences.
.onChange(of: selectedFruit) { newValue in
withAnimation {
message = "You picked \(newValue)!"
}
}
Animations give tactile feedback and increase interface polish.
Using Enum with Picker
Using an enum in place of a String or Int for Picker selections offers type safety and improves code clarity, especially in larger projects.
enum Fruit: String, CaseIterable, Identifiable {
case apple = "Apple"
case banana = "Banana"
case cherry = "Cherry"
var id: String { rawValue }
}
Usage in a Picker looks like this:
@State private var selectedFruit: Fruit = .apple
Picker("Choose a fruit", selection: $selectedFruit) {
ForEach(Fruit.allCases) { fruit in
Text(fruit.rawValue).tag(fruit)
}
}
.onChange(of: selectedFruit) { newFruit in
print("Selected fruit is now \(newFruit.rawValue)")
}
Using enums with Picker ensures better compile-time safety and makes your code easier to refactor in the future.
Best Practices
- Use .onChange whenever you need to react to Picker value updates—it’s clean and concise.
- Use enums instead of raw values for more maintainable code.
- Animate changes for smoother UX using
withAnimation. - Test for iOS version compatibility—.onChange is iOS 14+, so fall back to alternate methods for earlier versions.
Conclusion
Adding actions to Picker in SwiftUI is a frequent requirement, and although it isn’t directly built into the component like in UIKit, SwiftUI still offers multiple elegant solutions. Whether you prefer reactive bindings with .onChange, property observers, or Combine, you’ll find that SwiftUI gives you the flexibility to build responsive and intuitive interfaces. Experiment with different approaches and apply the one that matches your app’s architecture and needs.
The next time you add a Picker to a view, remember it’s more than a static control—it can become an active participant in your UI’s dynamics with just a few lines of smart code.



Leave a Reply