#State property keeping initial value instead of updating - ios

From a settings page, I want to :
Navigate to a child view
Let the user input update some value in a textfield
Save this value in the user defaults
Navigate back to the settings
If the user opens the child view again, pre-fill the textfield with the previously saved value
Given the following (simple) code :
// User defaults wrapper
class SettingsProvider: ObservableObject {
static let shared = SettingsProvider()
var savedValue: String {
get { UserDefaults.standard.string(forKey: "userdefaultskey") ?? "Default value" }
set {
UserDefaults.standard.setValue(newValue, forKey: "userdefaultskey")
objectWillChange.send()
}
}
}
struct SettingsView: View {
var body: some View {
NavigationView {
NavigationLink("Open child", destination: ChildView())
}
}
}
struct ChildView: View {
#ObservedObject var settingsProvider = SettingsProvider.shared
#State var text: String = SettingsProvider.shared.savedValue
var body: some View {
Text("Value is \(settingsProvider.savedValue)")
TextField("Enter value", text: $text).background(Color.gray)
Button("Save value") {
settingsProvider.savedValue = text
}
}
}
I'm having the following behaviour : video
Can somebody explain to me why the TextField contains Default value the second time I open it ?
Is it a bug in SwiftUI that I should report, or am I missing something ?
If I kill & re-open the app, the textfield will contain (as expected) Other value.

You can just add an onAppear { text = SettingsProvider.shared.savedValue } under the Button like this:
var body: some View {
Text("Value is \(settingsProvider.savedValue)")
TextField("Enter value", text: $text).background(Color.gray)
Button("Save value") {
settingsProvider.savedValue = text
}
.onAppear {
text = SettingsProvider.shared.savedValue // <= add this
}
}

Related

SwiftUI Alert dismisses itself due to Code Execution order

I have a view, where i can add a new entry to CoreData. The name for that entry cannot be null, which can be seen in the ViewModel. If someone tries to add a new entry without a name, they are presented with an error. Now, every time the error pops up, it dismisses itself.
The View:
struct AddProductPopover: View {
#Environment(\.presentationMode) var presentationMode
#StateObject var prodPopViewModel = AddProductPopoverViewModel()
var body: some View {
NavigationView {
List {
HStack {
Label("", systemImage: K.ProductIcons.name)
.foregroundColor(.black)
Spacer().frame(maxWidth: .infinity)
TextField("Add Name", text: $prodPopViewModel.newProductName)
.keyboardType(.default)
}
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Save") {
prodPopViewModel.saveProduct()
// if saving fails due to an empty name, the dismissal is still called before the error is displayed
presentationMode.wrappedValue.dismiss()
}
.alert(isPresented: $prodPopViewModel.showAlert) {
Alert(
title: Text("Product Name cannot be empty!"),
message: Text("Please specify a name for your new Product.")
)
}
}
}
}
}
The ViewModel:
class AddProductPopoverViewModel: ObservableObject {
var managedObjectContext = PersistenceController.shared.container.viewContext
#Published var newProductName: String = ""
#Published var newProductVendor: String = ""
#Published var newProductCategory: String = ""
#Published var newProductStoredQuantity: Int = 0
#Published var showAlert = false
func saveProduct() {
// if name is not nil saves the new product to CoreData
if !newProductName.isEmpty {
let newProduct = ProductEntity(context: managedObjectContext)
newProduct.productName = newProductName
newProduct.id = UUID()
newProduct.productVendor = newProductVendor
newProduct.productCategory = newProductCategory
newProduct.productStoredQuantity = Int32(newProductStoredQuantity)
PersistenceController.shared.save()
} else {
showAlert = true
}
}
I have figured out, that issue lies in the View in the Button Save action. Whenever the check in the ViewModel fails, it sets the boolean required for the alert to true. However, after setting that boolean to true, it returns to the view first and completes the next step in the Button Action, which is dismissing the current view before it then finally triggers the Alert. This execution order results in the Alert to be dismissed. However, the alert should not be dismissed. Dismissing should only happen if saving to CoreData has been successfull.
Button("Save") {
prodPopViewModel.saveProduct()
presentationMode.wrappedValue.dismiss()
}
What changes would I need to make to skip the dismissing line in case the boolean is set to true? I thought of including the dismissal in the ViewModel. However, that would violate the MVVM concept I'm trying to follow.
Replace save button with:
Button("Save") {
prodPopViewModel.saveProduct() // save the product
if (!prodPopViewModel.showAlert) { // don't dismiss if need to show alert
presentationMode.wrappedValue.dismiss()
}
}
.alert("Product Name cannot be empty!", // alert title
// decide to show alert i.e. save failed
isPresented: $prodPopViewModel.showAlert) {
Button("Ok"){
// hide alert on button press
prodPopViewModel.showAlert = false
}
} message: {
Text("Please specify a name for your new Product.")
}

What textfield / view to show and edit text in SwiftUI?

I would like to edit an item, for example, I have an item named "Jacket", then I would like to edit with a new name "Gray Jacket"
Now I'll have the list of my items, and when I press the "Jacket" item, it will go to the DetailItem and when it appears, I want that textfield / view already filled with "Jacket" text then I able to edit it. Please note I want to use real text not placeholder when the item name first shows up.
Does anyone know what element should I use, or if textfield how? Because I couldn't find how to set textfield with name when it first appears.
UPDATE:
Here's what I've tried, this code I took it from here. It basically took the previous text and make it a placeholder, while actually I want it to be editable.
struct CustomTextField: View {
var placeholder: Text
#Binding var text: String
var editingChanged: (Bool)->() = { _ in }
var commit: ()->() = { }
var body: some View {
ZStack(alignment: .leading) {
if text.isEmpty { placeholder }
TextField("", text: $text, onEditingChanged: editingChanged, onCommit: commit)
}
}
}
struct UsageCustomTxtField: View {
#State var text = ""
var body: some View {
CustomTextField(
placeholder: Text("placeholder").foregroundColor(.black),
text: $text
)
}
}
Here is a possible solution:
import SwiftUI
struct ContentView: View {
#ObservedObject var myItems: ItemViewModel = ItemViewModel()
#State var myNewName: String = "" // Will set the new name
var body: some View {
VStack {
TextField(self.myItems.myList[0].name, // when it appears, I want that textfield / view already filled with "Jacket" text
text: self.$myNewName,
onCommit: {
self.myItems.changeNameOfListItemTo(newName: self.myNewName, index: 0) I would like to edit with a new name "Gray Jacket"
self.myNewName = "" //empty the textfield again so name from item itself will be displayed
})
}
}
}
struct Item { // I have an item named "Jacket"
var name: String
init(name: String) {
self.name = name
}
}
class ItemViewModel: ObservableObject {
#Published var myList: Array<Item> = [Item(name: "Jacket")] //your item list
func changeNameOfListItemTo(newName: String, index: Int) { //function to change the name of an item
self.myList[0].name = newName
}
}

Picker data not updating when sheet is dismissed

I am using coredata to save information. This information populates a picker, but at the moment there is no information so the picker is empty. The array is set using FetchedRequest.
#FetchRequest(sortDescriptors: [])
var sources: FetchedResults<Source>
#State private var selectedSource = 0
This is how the picker is setup.
Picker(selection: $selectedSource, label: Text("Source")) {
ForEach(0 ..< sources.count) {
Text(sources[$0].name!)
}
}
There is also a button that displays another sheet and allows the user to add a source.
Button(action: { addSource.toggle() }, label: {
Text("Add Source")
})
.sheet(isPresented: $addSource, content: {
AddSource(showSheet: $addSource)
})
If the user presses Add Source, the sheet is displayed with a textfield and a button to add the source. There is also a button to dismiss the sheet.
struct AddSource: View {
#Environment(\.managedObjectContext) var viewContext
#Binding var showSheet: Bool
#State var name = ""
var body: some View {
NavigationView {
Form {
Section(header: Text("Source")) {
TextField("Source Name", text: $name)
Button("Add Source") {
let source = Source(context: viewContext)
source.name = name
do {
try viewContext.save()
name = ""
} catch {
let error = error as NSError
fatalError("Unable to save context: \(error)")
}
}
}
}
.navigationBarTitle("Add Source")
.navigationBarItems(trailing: Button(action:{
self.showSheet = false
}) {
Text("Done").bold()
.accessibilityLabel("Add your source.")
})
}
}
}
Once the sheet is dismissed, it goes back to the first view. The picker in the first view is not updated with the newly added source. You have to close it and reopen. How can I update the picker once the source is added by the user? Thanks!
The issue is with the ForEach signature you're using. It works only for constant data. If you want to use with changing data, you have to use something like:
ForEach(sources, id: \Source.name.hashValue) {
Text(verbatim: $0.name!)
}
Note that hashValue will not be unique for two entity objects with the same name. This is just an example

How to know whether the SecureField is editing

I'm using SecureField to build a login page. However, I notice there is no onEditingChanged, so we can't know whether it's become first responder or not. Is there any solution to monitor this event?
You can create a model class that holds the variable for secureText value (In your case it should be LoginModel or something like that). Then bind the value to secureField text and add didSet on the Published var in the model. Here is an example:
Model:
class SecureFieldModel: ObservableObject {
#Published var secureText = "" {
didSet {
print(secureText)
}
}
}
View:
#ObservedObject var model = SecureFieldModel()
struct LoginView: View {
var body: some View {
SecureField("Type your text here", text: $model.secureText)
}
}
I think you may misunderstand my question. If the textfield is just
focus, no text yet, we can't get the event. – RayChen 3 hours ago
Mantas answer slightly modified. "firing" when the input text has content or it is active:
struct ContentView: View {
#ObservedObject var model = SecureFieldModel()
var body: some View {
// No info when focus removed
SecureField("Type your text here", text: $model.secureText)
.onTapGesture { // when active
self.model.textFieldActivated()
}
// Do something when focus removed
SecureField("Type your text here", text: $model.secureText, onCommit: {print("Commited and focus removed")})
.onTapGesture { // when active
self.model.textFieldActivated()
}
}
}
class SecureFieldModel: ObservableObject {
#Published var secureText = "" {
didSet {
// if text
if secureText.count >= 1 {
print(secureText)
}
}
}
func textFieldActivated() {
if secureText.count >= 1 {
print(secureText)
} else {
print("is active")
}
}
}

How to detect what changed a #Published value in SwiftUI?

I have an ObservableObject with a #Published value, how can I detect if the value was changed via TextField view or it was set directly (When Button is tapped for instance)?
struct ContentView: View {
#ObservedObject var model = Model()
var body: some View {
VStack {
Button("set value") {
self.model.value = "user set value"
}
TextField("value", text: $model.value)
}
}
}
class Model: ObservableObject {
#Published var value = ""
var anyCancellable: AnyCancellable?
init() {
anyCancellable = $value.sink { val in
// if changed by Button then ...
// if changed by TextField then ...
}
}
}
My real case scenario sounds like this: when the user changes the value a request have to be sent to the server with the new value, but the server can also respond with a new value (in that case a new request to server should not be sent), so I have to distinguish between the case when the user changes the value (via TextField) and the case when the server changes the value.
You can do this with #Binding easily. I don't think you need to use #Published
If you still want to use it you can try this code
struct ContentView: View {
#ObservedObject var model = Model()
var body: some View {
VStack {
Button("set value") {
DispatchQueue.main.async{
self.model.value = "user set value"
}
}
TextField("value", text: $model.value)
}
}
}
You can pass an onEditingChanged closure to the TextField initializer. You should use the closure to trigger the server request.
struct ContentView: View {
#ObservedObject var model = Model()
var body: some View {
VStack {
Button("set value") {
self.model.value = "user set value"
}
TextField(
"value",
text: $model.value,
onEditingChanged: { _ in self.model.sendServerRequest() )
}
}
}

Resources