SwiftUI: Using onAppear doesn't allow Picker value to be changed

Hello,

I’m building a Edit View, that allows a user to make updates to a list item and save it back to core data and the list. To do this I am passing in the values from the list in the appropriate fields using the .onAppear action.

All data is passed in appropriately, and the toggle and text fields allow me to make changes easily and save them back to core data and update the list item. However, if I try to change the picker value (emojiChoice) and select a new one, the picker value does not change or save back to Core Data.

When I comment out the onAppear action for the picker variable (emojiChoice), the picker now allows me to choose the value I want and save it back to Core Data. I also have another view that allows a user to create items that is near identical to this Edit View, minus the onAppear action, that works as well. The picker allows a user to choose and emoji. Because there are alot of emoji’s, I created an array and instead am just passing the array index of a selected emoji as an Int.

How can I fix this to allow the picker to show the value to be edited and also allow it to be changed?

Here’s the code of the Edit View:

import CoreData
import SwiftUI

struct editRemindr: View {
@Environment(\.managedObjectContext) var moc
@Environment(\.presentationMode) var presentationMode

let emojiList = EmojiList()
@ObservedObject var reminder: ReminderEntity
@State private var showingDeleteAlert = false

// Form Variables
@State var notifyOn = true
@State var emojiChoice = 0
@State var notification: String
@State var notes: String

// Delete Reminder Function
func deleteAction() {
    moc.delete(reminder)
    // try? self.moc.save()
    presentationMode.wrappedValue.dismiss()
}

// View Controller
var body: some View {
    Form {

        // On/Off Toggle
        Toggle(isOn: $notifyOn) {
            Text("On/Off")
        }

        // Emoji Picker
        Picker(selection: $emojiChoice, label: Text("Emoji")) {
            ForEach(0 ..< emojiList.emojis.count) {
                Text(self.emojiList.emojis[$0])
            }
        }

        // Notification Text
        Section(header: Text("NOTIFICATION")) {
            HStack {
                TextField("Write your notification...", text: $notification)
                    .onReceive(notification.publisher.collect()) {
                        self.notification = String($0.prefix(60)) // <---- SET CHARACTER LIMIT
                }
                Text("\(notification.count) / 60")
                    .font(.caption)
                    .foregroundColor(.gray)
            }
        }

        // Notes Text
        Section(header: Text("NOTES")) {
            VStack {
                MultiLineTextField(text: $notes).frame(numLines: 6)
                    .padding(.top, 5)
                    .onReceive(notes.publisher.collect()) {
                        self.notes = String($0.prefix(240)) // <---- SET CHARACTER LIMIT
                }
                Text("\(notes.count) / 240")
                    .font(.caption)
                    .foregroundColor(.gray)
                    .frame(maxWidth: .infinity, alignment: .trailing)
            }
        }

        // Save Changes Button
        Section() {
            Button (action: {
                self.reminder.dateCreated = Date()
                self.reminder.notifyOn = self.notifyOn
                self.reminder.emojiChoice = Int64(self.emojiChoice)
                self.reminder.notification = self.notification
                self.reminder.notes = self.notes

                try? self.moc.save()

                self.presentationMode.wrappedValue.dismiss()
            }) {
                Text("SAVE CHANGES")
                    .fontWeight(.bold)
                    .foregroundColor(.white)
                    .font(.body)
            }        .padding()
                .frame(maxWidth: .infinity)
                .background(Color.green)
                .padding(.vertical, -6)
                .padding(.horizontal, -15)

            // Cancel Button
            Button(action: {
                self.presentationMode.wrappedValue.dismiss()
            }) {
                Text("Cancel")
                    .frame(maxWidth: .infinity)
            }
        }
    }

    // Make List Items Appear in Fields
    .onAppear(perform: {
        self.notifyOn = self.reminder.notifyOn
        self.emojiChoice = Int(self.reminder.emojiChoice)
        self.notification = self.reminder.notification ?? "unknown"
        self.notes = self.reminder.notes ?? "unknown"
    }) 
        .alert(isPresented: $showingDeleteAlert) {
            Alert(title: Text("Delete Reminder"), message: Text("Are you sure you want to delete this Reminder?"), primaryButton: .destructive(Text("Delete")) {
                self.deleteAction()
                }, secondaryButton: .cancel()
            )
    }
    .navigationBarTitle("Edit Reminder")
    .navigationBarItems(trailing: Button(action: {self.showingDeleteAlert = true
    }) {
        Image(systemName: "trash")

    })
} 

// Creates Text Limits
class TextLimit: ObservableObject {
    @Published var text = "" {
        didSet {
            if text.count > characterLimit && oldValue.count <= characterLimit {
                text = oldValue
            }
        }
    }
    let characterLimit: Int

    init(limit: Int = 5){
        characterLimit = limit
    }

}

}

// MARK: - PREVIEW GENERATOR
struct editRemindr_Previews: PreviewProvider {
static let moc = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)

static var previews: some View {
    let reminder = ReminderEntity(context: moc)
    reminder.notifyOn = true
    reminder.emojiChoice = 3
    reminder.notification = "Test Notification"
    reminder.notes = "Test Notification Notes"

    return NavigationView {
        editRemindr(reminder: reminder, notification: "Test Notification", notes: "Test Notification Notes")
    }
  }
}

Here’s the code for the Content View with the list if it helps as well.

import SwiftUI
import CoreData

struct ContentView: View {

@Environment(\.managedObjectContext) var moc
@FetchRequest(entity: ReminderEntity.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \ReminderEntity.dateCreated, ascending: false)])
var reminder: FetchedResults<ReminderEntity>

@State private var showingAddScreen = false
@State var showWelcomeScreen = false

let emojiList = EmojiList()

//Toggle Control
@State var notifyOn = true

// Save Items Function
func saveItems() {
    do {
        try moc.save()
    } catch {
        print(error)
    }
}

// Delete Item Function
func deleteItem(indexSet: IndexSet) {
    let source = indexSet.first!
    let listItem = reminder[source]
    moc.delete(listItem)
}

// View Controller
var body: some View {
    VStack {
        NavigationView {
            ZStack (alignment: .top) {

                // List View
                List {
                    ForEach(reminder, id: \.self) { notification in
                        NavigationLink(destination: editRemindr(reminder: notification, notification: notification.notification ?? "unknown", notes: notification.notes ?? "unknown")) {
                            // Text within List View
                            HStack {
                                // MARK: TODO
                                // Toggle("NotifyOn", isOn: true)
                                // .labelsHidden() // Hides the label/title
                                Text("\(self.emojiList.emojis[Int(notification.emojiChoice)]) \(notification.notification!)")
                            } 
                        }
                    } 
                    .onDelete(perform: deleteItem)
                }

                    // Navigation Items
                    .navigationBarTitle("", displayMode: .inline)
                    .navigationBarItems(
                        leading:
                        HStack {

                            Button(action: {
                                self.showWelcomeScreen.toggle()
                            }) {
                                Image(systemName: "info.circle.fill")
                                    .font(.system(size: 24, weight: .regular))
                            }.foregroundColor(.gray)

                            // Positioning Remindr Logo on Navigation
                            Image("remindrLogoSmall")
                                .resizable()
                                .aspectRatio(contentMode: .fit)
                                //.frame(width: 60, height: 60, alignment: .center)
                                .padding(.leading, 83)
                                .padding(.top, -10)
                        },

                        // Global Settings Navigation Item
                        trailing: NavigationLink(destination: globalSettings()){
                            Image("settings")
                                .font(Font.title.weight(.ultraLight))
                        }.foregroundColor(.gray)
                )

                // Add New Reminder Button
                VStack {
                    Spacer()
                    Button(action: { self.showingAddScreen.toggle()
                    }) {
                        Image("addButton")
                            .renderingMode(.original)
                    }
                    .sheet(isPresented: $showingAddScreen) {

                        newRemindr().environment(\.managedObjectContext, self.moc)

                    } 
                }
            }
        }  .sheet(isPresented: $showWelcomeScreen) {
            welcomeScreen()
        }
    }
}
}

// MARK: - PREVIEW GENERATOR
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
    ContentView()
  }
}

Any help would be greatly appreciated! Thank you so much!

Hi
I am facing same problem here

I want to initialize value of picker based on argument sent by parent view
I changed the value of the selector in OnAppear()
but the the selector doesn’t update its value

Can you show the code you have for that Picker View (and any relevant code external to that View that it relies on).

Paste your code in as text rather than a screenshot so that some testing can be done and a potential solution offered.

To format the code nicely, place 3 back-ticks ``` on the line above your code and 3 back-ticks ``` on the line below your code. Like this:

```
Code goes here
```

The 3 back-ticks must be the ONLY characters on the line. The back-tick character is located on the same keyboard key as the tilde character ~ (which is located below the Esc key - QWERTY keyboard).

Alternatively after you paste in your code, select that code block and click the </> button on the toolbar. This does the same thing as manually typing the 3 back ticks on the line above and below your code.

Hi Chris,

After trying more combination I found out the solution:

The following code doesn’t work.

struct WordPropDetailView: View {
    
    @State var lItem: Words

    @State var sel : Int = 0
    
    var body: some View {
        Form {
            
            
            Picker("Part Of Speech", selection: $sel) {
                Text("Noun").tag(0)
                Text("Pronoun").tag(1)
                Text("Adjective").tag(2)
            }
            
        }.onAppear() { // <-- picker does not update inspire of change in self value if onAppear is placed here
                sel = 1
                
         }
    }
}

what I did was I transfer the .OnAppear from Form{} to Picker{}

@State var lItem: Words

    @State var sel : Int = 0
    
    var body: some View {
        Form {
            
            
            Picker("Part Of Speech", selection: $sel) {
                Text("Noun").tag(0)
                Text("Pronoun").tag(1)
                Text("Adjective").tag(2)
            }.onAppear() { //<-- Fixed
                sel = 1
                
            }
            
        }
    }

Yeah that’s likely to be a timing issue with .onAppear(). I have encountered that behaviour in another project I was messing about with. Now that you have that on the Picker View rather than the Form { } it updates as expected.

SwiftUI goes through multiple screen refreshes as it builds the View so the process is from the outer View (the body) then the Form, then the Picker, then the modifier (.onAppear) on the Picker. It creates the View so fast that you don’t see the steps in the process. That’s a testament to the processing speed that it is not perceptible to the eye.