I want to change another unrelated @State
variable when a Picker
gets changed, but there is no onChanged
and it's not possible to put a didSet
on the pickers @State
. Is there another way to solve this?
9 Answers
Deployment target of iOS 14 or newer
Apple has provided a built in onChange
extension to View
, which can be used like this:
struct MyPicker: View {
@State private var favoriteColor = 0
var body: some View {
Picker(selection: $favoriteColor, label: Text("Color")) {
Text("Red").tag(0)
Text("Green").tag(1)
}
.onChange(of: favoriteColor) { tag in print("Color tag: \(tag)") }
}
}
Deployment target of iOS 13 or older
struct MyPicker: View {
@State private var favoriteColor = 0
var body: some View {
Picker(selection: $favoriteColor.onChange(colorChange), label: Text("Color")) {
Text("Red").tag(0)
Text("Green").tag(1)
}
}
func colorChange(_ tag: Int) {
print("Color tag: \(tag)")
}
}
Using this helper
extension Binding {
func onChange(_ handler: @escaping (Value) -> Void) -> Binding<Value> {
return Binding(
get: { self.wrappedValue },
set: { selection in
self.wrappedValue = selection
handler(selection)
})
}
}

- 8,438
- 7
- 41
- 48
-
1This is a good solution, but as SwiftUI matures, I would expect a native solution. – William Grand May 01 '20 at 18:18
-
This is a brilliant solution! Far better than `.onReceive` since that was called every time the View got rendered in my case. Thank you. – ixany Aug 04 '20 at 19:00
-
@WilliamGrand You were right, iOS 14 introduced [.onChange](https://www.hackingwithswift.com/quick-start/swiftui/how-to-run-some-code-when-state-changes-using-onchange) – Jeremy Aug 05 '20 at 15:17
-
Should have mentioned `tag` is important here otherwise `onChange` modifier won't be invoked in the latter case. I have not tried the first one though – Asesh Mar 01 '21 at 15:24
-
The iOS 14 solution worked perfectly! – dbDev Nov 09 '21 at 15:33
-
Thank you, solved my problem, helped me better understand both .tag() and .onChange(). My problem was, I wanted my picker bound to an Int in my view model. The .tag() and .onChange() helped me update the Int in sync with the Picker selection. Inside .onChange perform { tag in vm.foo = tag }. – Tim Sonner Apr 06 '22 at 03:11
First of all, full credit to ccwasden for the best answer. I had to modify it slightly to make it work for me, so I'm answering this question hoping someone else will find it useful as well.
Here's what I ended up with (tested on iOS 14 GM with Xcode 12 GM)
struct SwiftUIView: View {
@State private var selection = 0
var body: some View {
Picker(selection: $selection, label: Text("Some Label")) {
ForEach(0 ..< 5) {
Text("Number \($0)") }
}.onChange(of: selection) { _ in
print(selection)
}
}
}
The inclusion of the "_ in" was what I needed. Without it, I got the error "Cannot convert value of type 'Int' to expected argument type '()'"

- 644
- 7
- 7
-
5This should be the accepted solution, simple and straight to the point. – Joe Scotto Dec 23 '20 at 20:39
-
-
-
I think this is simpler solution:
@State private var pickerIndex = 0
var yourData = ["Item 1", "Item 2", "Item 3"]
// USE this if needed to notify parent
@Binding var notifyParentOnChangeIndex: Int
var body: some View {
let pi = Binding<Int>(get: {
return self.pickerIndex
}, set: {
self.pickerIndex = $0
// TODO: DO YOUR STUFF HERE
// TODO: DO YOUR STUFF HERE
// TODO: DO YOUR STUFF HERE
// USE this if needed to notify parent
self.notifyParentOnChangeIndex = $0
})
return VStack{
Picker(selection: pi, label: Text("Yolo")) {
ForEach(self.yourData.indices) {
Text(self.yourData[$0])
}
}
.pickerStyle(WheelPickerStyle())
.padding()
}
}

- 2,526
- 1
- 25
- 24
I know this is a year old post, but I thought this solution might help others that stop by for a visit in need of a solution. Hope it helps someone else.
import Foundation
import SwiftUI
struct MeasurementUnitView: View {
@State var selectedIndex = unitTypes.firstIndex(of: UserDefaults.standard.string(forKey: "Unit")!)!
var userSettings: UserSettings
var body: some View {
VStack {
Spacer(minLength: 15)
Form {
Section {
Picker(selection: self.$selectedIndex, label: Text("Current UnitType")) {
ForEach(0..<unitTypes.count, id: \.self) {
Text(unitTypes[$0])
}
}.onReceive([self.selectedIndex].publisher.first()) { (value) in
self.savePick()
}
.navigationBarTitle("Change Unit Type", displayMode: .inline)
}
}
}
}
func savePick() {
if (userSettings.unit != unitTypes[selectedIndex]) {
userSettings.unit = unitTypes[selectedIndex]
}
}
}

- 597
- 8
- 18
-
This actually solved another problem for me. I find that `onReceive` is a little too sensitive, and got into an infinite loop when it triggered an indirect change to the `@State` variable to itself. By adding `if value == oldValue { return }` it left this alone. – Manngo Oct 16 '20 at 05:37
-
-
It is nice but every time I tap on the picker it gets default first picker status. I am losing the last picker value during the process. How can I modify .publisher.first() to something else to avoid this problem. – Mert Köksal Aug 19 '21 at 08:51
-
.onChange(of: self.selectedStatusIndex) { newValue in self.editItem(account: account) } – Mert Köksal Aug 19 '21 at 09:10
I use a segmented picker and had a similar requirement. After trying a few things I just used an object that had both an ObservableObjectPublisher
and a PassthroughSubject
publisher as the selection. That let me satisfy SwiftUI and with an onReceive()
I could do other stuff as well.
// Selector for the base and radix
Picker("Radix", selection: $base.value) {
Text("Dec").tag(10)
Text("Hex").tag(16)
Text("Oct").tag(8)
}
.pickerStyle(SegmentedPickerStyle())
// receiver for changes in base
.onReceive(base.publisher, perform: { self.setRadices(base: $0) })
base has both an objectWillChange
and a PassthroughSubject<Int, Never>
publisher imaginatively called publisher.
class Observable<T>: ObservableObject, Identifiable {
let id = UUID()
let objectWillChange = ObservableObjectPublisher()
let publisher = PassthroughSubject<T, Never>()
var value: T {
willSet { objectWillChange.send() }
didSet { publisher.send(value) }
}
init(_ initValue: T) { self.value = initValue }
}
typealias ObservableInt = Observable<Int>
Defining objectWillChange isn't strictly necessary but when I wrote that I liked to remind myself that it was there.

- 1,056
- 7
- 15
For people that have to support both iOS 13 and 14, I added an extension which works for both. Don't forget to import Combine.
Extension View {
@ViewBuilder func onChangeBackwardsCompatible<T: Equatable>(of value: T, perform completion: @escaping (T) -> Void) -> some View {
if #available(iOS 14.0, *) {
self.onChange(of: value, perform: completion)
} else {
self.onReceive([value].publisher.first()) { (value) in
completion(value)
}
}
}
}
Usage:
Picker(selection: $selectedIndex, label: Text("Color")) {
Text("Red").tag(0)
Text("Blue").tag(1)
}.onChangeBackwardsCompatible(of: selectedIndex) { (newIndex) in
print("Do something with \(newIndex)")
}
Important note: If you are changing a published property inside an observed object within your completion block, this solution will cause an infinite loop in iOS 13. However, it is easily fixed by adding a check, something like this:
.onChangeBackwardsCompatible(of: showSheet, perform: { (shouldShowSheet) in
if shouldShowSheet {
self.router.currentSheet = .chosenSheet
showSheet = false
}
})

- 61
- 1
- 3
-
1The fallback solution for iOS 13 isn't working for me if value is a bool. – Dree Dec 18 '20 at 17:04
SwiftUI 1 & 2
Use onReceive
and Just
:
import Combine
import SwiftUI
struct ContentView: View {
@State private var selection = 0
var body: some View {
Picker("Some Label", selection: $selection) {
ForEach(0 ..< 5, id: \.self) {
Text("Number \($0)")
}
}
.onReceive(Just(selection)) {
print("Selected: \($0)")
}
}
}

- 46,897
- 22
- 145
- 209
iOS 14 and CoreData entities with relationships
I ran into this issue while trying to bind to a CoreData entity and found that the following works:
Picker("Level", selection: $contact.level) {
ForEach(levels) { (level: Level?) in
HStack {
Circle().fill(Color.green)
.frame(width: 8, height: 8)
Text("\(level?.name ?? "Unassigned")")
}
.tag(level)
}
}
.onChange(of: contact.level) { _ in savecontact() }
Where "contact" is an entity with a relationship to "level".
The Contact
class is an @ObservedObject var contact: Contact
saveContact
is a do-catch
function to try viewContext.save()
...

- 587
- 9
- 23
The very important issue : we must pass something to "tag" modifier of Picker item view (inside ForEach) to let it "identify" items and trigger selection change event. And the value we passed will return to Binding variable with "selection" of Picker.
For example :
Picker(selection: $selected, label: Text("")){
ForEach(data){item in //data's item type must conform Identifiable
HStack{
//item view
}
.tag(item.property)
}
}
.onChange(of: selected, perform: { value in
//handle value of selected here (selected = item.property when user change selection)
})

- 3,208
- 9
- 22
- 33

- 117
- 1
- 5