5 Proven Ways to Make Action in Picker SwiftUI Code

Posted by Devin Rosario
7
Nov 4, 2025
105 Views
Image

The SwiftUI Picker is a fundamental component for allowing users to select a single value from a set of options. While it handles the selection state automatically via two-way binding, modern app development often requires a side effect—an “action”—to run the moment the selection changes. This action might involve filtering a list, fetching new data from a server, logging an event, or updating a different part of the user interface.

Simply binding a selection to a @State variable is the first step, but how do you react to that change effectively, maintain clean code, and follow SwiftUI's declarative principles?

This guide dives deep into the five proven ways to implement selection change actions in SwiftUI, ensuring your code remains scalable, testable, and idiomatic for 2025 standards. Mastering these patterns is crucial as you scale your applications. For those whose projects are becoming complex or require specialized, locale-specific attention, understanding when to bring in professional support is vital. When dealing with highly demanding feature sets or regulatory requirements, collaborating with experienced teams can be the fastest path to market success. In such cases, expert partners in regions known for advanced app engineering—such as mobile app development in Georgia—can help teams maintain both efficiency and code integrity.


1. The onChange(of:perform:) Modifier (The Standard)

The onChange view modifier is, by far, the most idiomatic and clearest way to run code when any @State, @Binding, or other property wrapper changes value. Introduced specifically to handle these kinds of side effects, it separates the reaction logic from the primary UI definition, resulting in highly readable code.

The Mechanism

The Picker is bound to a mutable property such as a @State variable. You apply the onChange modifier directly to the Picker or one of its container views, specifying the bound variable as the target. SwiftUI automatically executes the closure passed to onChange immediately after the bound variable’s value is updated by the system following the user’s selection.

Why This is Preferred

It keeps the reactive logic directly adjacent to the property it’s reacting to, making the intent immediately clear. It’s also flexible—you can call external functions, update other state variables, or trigger networking requests directly within the closure. This remains the most common and recommended pattern for simple to medium complexity actions.

Honest Limitations

While excellent, placing complex, multi-step business logic directly in the onChange closure can lead to large and unwieldy views. If your action involves many distinct steps, move that logic into a dedicated function or a ViewModel (Pattern #3).


2. The Custom Binding Approach

This method is highly effective when you need to decouple the action logic from the main view’s state management, often used when creating reusable component libraries or passing a selection deep into a hierarchy.

The Mechanism

Instead of binding the Picker to a simple @State property, you create a custom Binding computed property. This custom Binding takes two closures—a getter and a setter.

  • The getter returns the current selection value.

  • The setter runs your custom action before assigning the new value.

When to Choose This

This pattern is ideal when the action needs to be executed before the new value is stored, or when the Picker is part of a complex, reusable component that should manage its own side effects without relying on the parent view’s implementation. It’s a clean way to inject an action directly into the binding pipeline itself.


3. The ObservableObject Model-Driven Approach

For applications utilizing MVVM (Model-View-ViewModel) or similar architecture, the ViewModel is the natural place for all business logic, including actions triggered by UI events.

The Mechanism

  1. The Picker is bound to a published property within your @StateObject or @ObservedObject ViewModel.

  2. The ViewModel uses Swift property observers (didSet or willSet) on that @Published property.

  3. When the user changes the selection, the property updates and automatically triggers the observer, where your action logic resides.

Strategic Framework

Imagine a simple flow:

  • The View contains the Picker and binds it to a variable in the ViewModel.

  • Whenever the user selects a new option, the ViewModel detects the change and runs the associated logic, such as updating data or fetching new content.
    This setup keeps the user interface focused purely on presentation while the ViewModel handles all decision-making and business processing.


4. Using Switch Statements for Enumerated Pickers

When your Picker is based on an enum, which is a best practice for strongly typed selections, the action you run often depends on which specific case is selected.

The Mechanism

This is an advanced implementation of the onChange pattern. Instead of running a single generic function, you use a switch statement to handle each enum case individually.

Why It’s a Best Practice

Using a switch statement forces explicit handling of every possible selection, ensuring your app’s behavior remains predictable and robust. If a new enum case is added later, the Swift compiler warns you to update the action logic, helping you prevent missing behaviors or inconsistencies.

Method Primary Use Case Complexity Level Cleanliness
onChange Modifier Simple, direct actions Low High
Custom Binding Component reusability Medium Medium
ObservableObject Complex data logic High High (Decoupled)
Switch in onChange Enum-specific logic Medium High

5. The Swift didSet / willSet on State Property

Before the official onChange modifier became the standard, or in cases where you want to keep the side effect logic entirely within the @State declaration, Swift’s built-in property observers were the traditional solution.

The Mechanism

You define your @State property and append a didSet or willSet block directly to it. The code inside runs immediately after the property’s value changes.

When to Use This

While onChange is now preferred, property observers are still a clean way to handle very localized, simple side effects such as logging or updating small bits of related state. Just be careful not to mix too much logic into your view’s property declarations, as it can reduce readability over time.


Key Takeaways

  • The Standard: Use the onChange(of:) modifier (Pattern #1) for most Picker actions.

  • For Complexity: When actions involve networking or multi-layer logic, use the ObservableObject model-driven approach (Pattern #3).

  • For Robustness: Use the Switch statement (Pattern #4) when working with enums.

  • Avoid Old Patterns: Avoid UIKit’s target-action system unless absolutely necessary, as it breaks SwiftUI’s declarative model.


Next Steps

Integrate the onChange modifier into your current SwiftUI projects. Identify older or legacy code using non-idiomatic patterns and refactor toward the modern, clean, and maintainable approaches outlined here.


Frequently Asked Questions

Why is onChange better than calling a function inside the Picker’s body?
The view body should remain a pure description of UI state, not a place for side effects. Actions inside the body run every time the view updates, which causes unpredictable behavior. onChange runs only when the actual selection value changes.

Does onChange run when the view first loads?
Yes, it runs once when the view appears. If you want to skip the initial call, add a small Boolean flag to ignore the first update.

What if I need this feature on older iOS versions without onChange?
Use property observers (didSet) on @State or an ObservableObject pattern as fallbacks. These methods remain compatible with older frameworks.

Can I run asynchronous code inside onChange?
Yes. The closure supports async/await, allowing safe and non-blocking data fetching when the selection changes.

Should I use @State or @Binding for the Picker’s selection?
Use @State if the current view owns the value. Use @Binding if the value comes from a parent view and needs to propagate upward.

Comments
avatar
Please sign in to add comment.