This is a module where the user enters transaction data and then it is saved to coreData. EntryView calls getFormData for entry of form data then calls the function saveButton() for saving the data to coreData.
Things have been working great until I recently added two additional parameters gotCountry and gotHome. These parameters are defined in EntryView. The data I want is found in getFormData but I don't want to make it available to other parts of the app until the save button is pressed (func saveButton()) hence I need to pass the data from getFormData to saveButton().
One of the two warnings is Initialization of immutable value 'gotCountry' was never used; consider replacing with assignment to '_' or removing it if I place let in front of the parameters gotCountry and gotHome in getFormData. These are variables so shouldn't have let in front of them. Removing 'let' results in the error Type '()' cannot conform to 'View'
The parameters entryDT and entryPT are coming from form input data while gotCountry and gotHome are coming from calculated data available at time of entry.
Note that I have stripped out some of the code to see the passing of data better.
struct EntryView: View {
#EnvironmentObject var ctTotals: CountryTotals
#State private var entryDT = Date()
#State private var entryPT: Int = 0
#State private var gotCountry: String = ""
#State private var gotHome: Double = 0.0
var body: some View {
VStack (alignment: .leading){
ShowTabTitle(g: g, title: "Enter Transaction")
getFormData(entryDT: $entryDT, entryPT: $entryPT, gotCountry: $gotCountry, gotHome: $gotHome)
Button {
self.saveButton() // button pressed
} label: {
Text ("Save")
}
}
}
func saveButton() {
// save entry to core data
let newEntry = CurrTrans(context: viewContext)
// entry id
newEntry.id = UUID()
// entry date
newEntry.entryDT = entryDT
// entry payment type
newEntry.entryPT = Int64(entryPT)
ctTotals.sendTotals(gotCountry: gotCountry, gotHome: gotHome)
do {
try viewContext.save()
} catch {
}
// reset parameters for next entry
self.entryDT = Date()
self.entryPT = 0
}
}
struct getFormData: View {
#Binding var entryDT: Date
#Binding var entryPT: Int
#Binding var gotCountry: String
#Binding var gotHome: Double
var body: some View {
// get entry date and time
DatePicker("", selection: $entryDT, in: ...Date())
// select payment type
Picker(selection: $entryPT, label: Text("")) {}
// copy data to totals by country
gotCountry = currencies.curItem[userData.entryCur].cunName
gotHome = totalValue
}
}
struct CtModel: Codable, Identifiable, Hashable {
var id = UUID()
var ctName: String
var ctHome: Double
}
class CountryTotals: ObservableObject {
#Published var ctItem: [CtModel] {
didSet {
if let encoded = try? JSONEncoder().encode(ctItem) {
UserDefaults.standard.set(encoded, forKey: StorageKeys.ctTotals.rawValue)
}
}
}
init() {
}
func sendTotals(gotCountry: String, gotHome: Double) -> () {
let item = CtModel(ctName: gotCountry, ctHome: gotHome)
ctItem.append(item)
}
}
Related
I'm having issues pulling data from an Array into a picker using SwiftUI. I can correctly make a list of the data I'm interested in, but can't seem to make the same logic work to pull the data into a picker. I've coded it a few different ways but the current way I have gives this error:
Referencing initializer 'init(_:content:)' on 'ForEach' requires that 'Text' conform to 'TableRowContent'
The code is below:
import SwiftUI
struct BumpSelector: View {
#ObservedObject var model = ViewModel()
#State var selectedStyle = 0
init(){
model.getData2()}
var body: some View {
VStack{
List (model.list) { item in
Text(item.style)}
Picker("Style", selection: $selectedStyle, content: {
ForEach(0..<model.list.count, content: { index in
Text(index.style)
})
})
}
}
The model is here:
import Foundation
struct Bumps: Identifiable{
var id: String
var style: String
}
and the ViewModel is here:
import Foundation
import Firebase
import FirebaseFirestore
class ViewModel: ObservableObject {
#Published var list = [Bumps]()
#Published var styleArray = [String]()
func getData2() {
let db = Firestore.firestore()
db.collection("bumpStop").getDocuments { bumpSnapshot, error in
//Check for errors first:
if error == nil {
//Below ensures bumpSnapshot isn't nil
if let bumpSnapshot = bumpSnapshot {
DispatchQueue.main.async {
self.list = bumpSnapshot.documents.map{ bump in
return Bumps(id: bump.documentID,
style: bump["style"] as? String ?? "")
}
}
}
}
else {
//Take care of the error
}
}
}
}
index in your ForEach is just an Int, there is no style associated with an Int. You could try this approach to make the Picker work with its ForEach:
struct BumpSelector: View {
#ObservedObject var model = ViewModel()
#State var selectedStyle = 0
init(){
model.getData2()
}
var body: some View {
VStack{
List (model.list) { item in
Text(item.style)}
Picker("Style", selection: $selectedStyle) {
ForEach(model.list.indices, id: \.self) { index in
Text(model.list[index].style).tag(index)
}
}
}
}
}
EDIT-1:
Text(model.list[selectedStyle].style) will give you the required style of the selectedStyle.
However, as always when using index, you need to ensure it is valid at the time of use.
That is, use if selectedStyle < model.list.count { Text(model.list[selectedStyle].style) }.
You could also use this alternative approach that does not use index:
struct Bumps: Identifiable, Hashable { // <-- here
var id: String
var style: String
}
struct BumpSelector: View {
#ObservedObject var model = ViewModel()
#State var selectedBumps = Bumps(id: "", style: "") // <-- here
init(){
model.getData2()
}
var body: some View {
VStack{
List (model.list) { item in
Text(item.style)
}
Picker("Style", selection: $selectedBumps) {
ForEach(model.list) { bumps in
Text(bumps.style).tag(bumps) // <-- here
}
}
}
.onAppear {
if let first = model.list.first {
selectedBumps = first
}
}
}
}
Then use selectedBumps, just like any Bumps, such as selectedBumps.style
I am currently using an api to grab the definitions for a specific word that the user has entered, and the api returns multiple definitions. I want the user to be able to choose what exact definition they want to pair a word with. Since I am interacting with an api, it is in a function and I cannot return anything out of it. I want to grab all the definitions and then show a new view where the user can pick the appropriate definition. How can I go about doing this? I've thought of making an ObservableObject that just has an array as a work around, but that seems a bit excessive. I am new to SwiftUI, so I am unsure whether or not this would be possible. However, I think it would not be because I am not trying to return a view anywhere or using any of the built in things that accepts views.
EDIT: I made SaveArray an ObservableObject and now my problem is that the object is not being updated by my getDef function call. Within the function it is but it is not editing the actual class or at least that is what it is looking like, because on my next view I have a foreach going through the array and nothing is displayed because it is empty. I am not sure whether that is because the sheet is being brought up before the getDef function is done executing.
struct AddWord: View {
#ObservedObject var book: Book
#ObservedObject var currentArray = SaveArray()
#State var addingDefinition = false
#State var word = ""
#State var definition = ""
#State var allDefinitions: [String] = []
#Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView {
Form {
TextField("Word: ", text: $word)
}
.navigationBarTitle("Add word")
.navigationBarItems(trailing: Button("Add") {
if self.word != "" {
book.words.append(self.word)
getDef(self.word, book, currentArray)
addingDefinition = true
self.presentationMode.wrappedValue.dismiss()
}
}).sheet(isPresented: $addingDefinition) {
PickDefinition(definitions: currentArray, book: book, word: self.word)
}
}
}
func getDef(_ word: String, _ book: Book, _ definitionsArray: SaveArray) {
let request = NSMutableURLRequest(url: NSURL(string: "https://wordsapiv1.p.rapidapi.com/words/\(word)")! as URL,
cachePolicy: .useProtocolCachePolicy,
timeoutInterval: 10.0)
request.httpMethod = "GET"
request.allHTTPHeaderFields = headers
let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
if (error != nil) {
print(error)
} else {
do {
let dictionary = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? [String:Any]
//getting the dictionary
let dict = dictionary?["results"] as? [Any]
definitionsArray.currentArray = createArray((dict!))
}
catch {
print("Error parsing")
}
}
})
dataTask.resume()
}
func createArray(_ array: [Any]) -> [String] {
//Get all the definitions given from the api and put it into a string array so you can display it for user to select the correct definiton for their context
var definitions = [String]()
for object in array {
let dictDef = object as? [String: Any]
definitions.append(dictDef?["definition"] as! String)
}
return definitions
}
}
struct AddWord_Previews: PreviewProvider {
static var previews: some View {
AddWord(book: Book())
}
}
struct PickDefinition: View {
#ObservedObject var definitions: SaveArray
var book: Book
var word: String
#Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView {
VStack {
ForEach(0 ..< definitions.currentArray.count) { index in
Button("\(self.definitions.currentArray[index])", action: {
print("hello")
DB_Manager().addWords(name: self.book.name, word: self.word, definition: self.definitions.currentArray[index])
book.definitions.append(self.definitions.currentArray[index])
self.presentationMode.wrappedValue.dismiss()
})
}
}
.navigationTitle("Choose")
}
}
}
struct PickDefinition_Previews: PreviewProvider {
static var previews: some View {
PickDefinition(definitions: SaveArray(), book: Book(), word: "")
}
}
If you can post more of your code, I can provide a fully working example (e.g. the sample JSON and the views/classes you have built). But for now, I am working with what you provided. I hope the below will help you see just how ObservableObject works.
#Published var dict = [String]() //If the api returns a list of strings, you can make this of type string - I do not have a sample of the JSON so I cannot be sure. If you can provide a sample of the JSON I can better define the way this should work.
var body: some View {
let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
if (error != nil) {
print(error)
} else {
do {
let dictionary = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? [String:Any]
//getting the dictionary
dict = dictionary?["results"] as? [Any] //note that here we are assigning the results of the api call to the Published variable, which our StateObject variable in ContentView is listening to!
var allDef = createArray((dict!))
//No longer need to pass this data forward (as you have below) since we are publishing the information!
//Pass the array to the new view where the user will select the one they want
PickDefinition(definitions: allDef, book: self.book, word: self.word)
}
catch {
print("Error parsing")
}
}
})
dataTask.resume()
}
}
struct ContentView: View {
//StateObject receives updates sent by the Published variable
#StateObject var dictArray = Api()
var body: some View {
NavigationView {
List {
ForEach(dictArray.dict.indices, id: \.self) { index in
Text(dictArray.dict[index])
}
}
}
}
}
I've been using #AppStorage and UserDefaults for updates in SwiftUI. If I make a change to the vie that has the #AppStorage wrapper all works well. I'm confused with how to make this work globally.
I'm using a struct that has computed properties and formatters associated. The idea is to check user defaults and convert items to lbs or kg. The issue is that the views using the computed properties do not update when UserDefaults is updated. Is there a way to create a global change that would update weightFormatted in SecondaryView below?
// Weight Struct
struct Weight {
var weight: Double
var weightFormatted: String {
return weightDecimalLbsOrKgFormatted2(weight)
}
// Formatting Method
func weightDecimalLbsOrKgFormatted2(_ lbs: Double) -> String {
if (!UserDefaults.standard.bool(forKey: "weightInKilograms")) {
let weightString = decimalFormatterDecimal2(lbs)
return weightString + "lbs"
} else {
let kg = toKg(lbs)
let weightString = decimalFormatterDecimal2(kg)
return weightString + "kg"
}
}
// Where weightInKilograms Is Set
struct AccountView: View {
#AppStorage("weightInKilograms") var weightInKilograms = false
let weight = Weight(weight: 9.0))
var body: some View {
VStack {
Text(weight.weightFormatted)
Toggle(isOn: $weightInKilograms) {
Text("Kilograms")
}
}
}
}
// Secondary View Not Updating
struct SecondaryView: View {
let weight = Weight(weight: 9.0))
var body: some View {
Text(weight.weightFormatted)
}
}
Your problem is that weight isn't wrapped by any state.
In your AccountView, give weight a #State wrapper:
struct AccountView: View {
#AppStorage("weightInKilograms") var weightInKilograms = false
#State var weight = Weight(weight: 9.0))
var body: some View {
//...
}
}
In SecondaryView, ensure that weight is wrapped with #Binding:
struct SecondaryView: View {
#Binding var weight: Weight
var body: some View {
// ...
}
}
Then, pass weight as a Binding<Weight> variable to SecondaryView within your first View:
SecondaryView(weight: $weight)
Is there a way to create a global change that would update weightFormatted in SecondaryView below?
If you're looking to make a global change, you should consider setting up a global EnvironmentObject:
class MyGlobalClass: ObservableObject {
// Published variables will update view states when changed.
#Published var weightInKilograms: Bool
{ get {
// Get UserDefaults here
} set {
// Set UserDefaults here
}}
#Published var weight: Weight
}
If you pass an instance of MyGlobalClass as an EnvironmentObject to your main view, then to your secondary view, any changes made to properties in the global instance will update the views' state via the #Published wrapper:
let global = MyGlobalClass()
/* ... */
// In your app's lifecycle, or where AccountView is instantiated
AccountView().environmentObject(global)
struct AccountView: View {
#EnvironmentObject var global: MyGlobalClass
var body: some View {
// ...
Text(global.weight.weightFormatted)
// ...
SecondaryView().environmentObject(global)
}
}
I'm working on a validation routine for a form, but when the validation results come in, the onChange is not being triggered.
So I have a form that has some fields, and some nested items that have some more fields (the number of items may vary). Think of a form for creating teams where you get to add people.
When the form is submitted, it sends a message to each item to validate itself, and the results of the validation of each item are stored in an array of booleans. Once all the booleans of the array are true, the form is submitted.
Every time a change occurs in the array of results, it should change a flag that would check if all items are true, and if they are, submits the form. But whenever I change the flag, the onChange I have for it never gets called:
final class AddEditProjectViewModel: ObservableObject {
#Published var array = ["1", "2", "3", "hello"]
// In reality this array would be a collection of objects with many properties
}
struct AddEditItemView: View {
#State var text : String
#Binding var doValidation: Bool // flag to perform the item validation
#Binding var isValid : Bool // result of validating all fields in this item
init(text: String, isValid: Binding<Bool>, doValidation: Binding<Bool>) {
self._text = State(initialValue: text)
self._isValid = isValid
self._doValidation = doValidation
}
func validateAll() {
// here would be some validation logic for all form fields,
//but I'm simulating the result to all items passed validation
// Validation needs to happen here because there are error message
//fields within the item view that get turned on or off
isValid = true
}
var body: some View {
Text(text)
.onChange(of: doValidation, perform: { value in
validateAll() // when the flag changes, perform the validation
})
}
}
struct ContentView: View {
#ObservedObject var viewModel : AddEditProjectViewModel
#State var performValidateItems : Bool = false // flag to perform the validation of all items
#State var submitFormFlag = false // flag to detect when validation results come in
#State var itemsValidationResult = [Bool]() // store the validation results of each item
{
didSet {
print(submitFormFlag) // i.e. false
submitFormFlag.toggle() // Even though this gets changed, on changed on it won't get called
print(submitFormFlag) // i.e. true
}
}
init(viewModel : AddEditProjectViewModel) {
self.viewModel = viewModel
var initialValues = [Bool]()
for _ in (0..<viewModel.array.count) { // populate the initial validation results all to false
initialValues.append(false)
}
_itemsValidationResult = State(initialValue: initialValues)
}
//https://stackoverflow.com/questions/56978746/how-do-i-bind-a-swiftui-element-to-a-value-in-a-dictionary
func binding(for index: Int) -> Binding<Bool> {
return Binding(get: {
return self.itemsValidationResult[index]
}, set: {
self.itemsValidationResult[index] = $0
})
}
var body: some View {
HStack {
ForEach(viewModel.array.indices, id: \.self) { i in
AddEditItemView(
text: viewModel.array[i],
isValid: binding(for: i),
doValidation: $performValidateItems
)
}
Text(itemsValidationResult.description)
Button(action: {
performValidateItems.toggle() // triggers the validation of all items
}) {
Text("Validate")
}
.onChange(of: submitFormFlag, perform: { value in // this never gets called
print(value, "forced")
// if all validation results in the array are true, it will submit the form
})
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(viewModel: AddEditProjectViewModel())
}
}
You shouldn't use didSet on the #State - it's a wrapper and it doesn't behave like standard properties.
See SwiftUI — #State:
Declaring the #State isFilled variable gives access to three
different types:
isFilled — Bool
$isFilled — Binding
_isFilled — State
The State type is the wrapper — doing all the extra work for us — that stores an underlying wrappedValue,
directly accessible using isFilled property and a projectedValue,
directly accessible using $isFilled property.
Try onChange for itemsValidationResult instead:
var body: some View {
HStack {
// ...
}
.onChange(of: itemsValidationResult) { _ in
submitFormFlag.toggle()
}
.onChange(of: submitFormFlag) { value in
print(value, "forced")
}
}
You may also consider putting the code you had in .onChange(of: submitFormFlag) inside the .onChange(of: itemsValidationResult).
I have a project which uses Core Data to store measurement values. The user can add new measurements to be persisted, and the user can edit persisted measurements.
The issue that I am experiencing is seen when attempting to edit a persisted measurement. After selecting a persisted measurement, the user is presented with the view to edit and save the measurement. The selected measurement is passed from the list to the presented view, where the value populates a TextField. Unfortunately, the value does not populate the TextField when the view is presented the first time within the app. Only after the second presentation does the measurement value populate the TextField.
The user can present the view to add a new measurement to be persisted, cancel and dismiss it, select an existing measurement, and that measurement's value will be displayed in the presented TextField. It seems that the initial presentation of the view used to add/edit a measurement does not contain the selected measurement on the first presentation. Only after the first presentation and dismissal will the value populate the TextField.
Below, you can see a 22sec GIF, which displays the current behavior.
In the GIF, you can see that a persisted measurement is selected, and the presented view's TextField is not populated with the measurement's value. Only on the second presentation is it populated. The last half of the GIF shows the process for persisting a new measurement and that the TextField is populated with that measurement's value on a subsequent presentation.
If you wish to reproduce the described behavior, then you can find the project's repository here, using the feature/edit-measurement branch, which the URL points to.
Steps to Reproduce
Launch application
Select any parameter from the list
Tap the trailing navigation button
Enter a value into the TextField
Tap the Save button
Return to the root view (list of parameters)
Select the parameter of which you saved the measurement value in step 5
Select the newly-persisted measurement in the list
Notice the unpopulated TextField
Tap the cancel button or dismiss the view by dragging downward
Select the same measurement that was selected in step 7
Notice the populated TextField
Below is the implementation of the view that displays persisted measurements:
import SwiftUI
struct ParameterMeasurementsLogView: View {
// MARK: Properties
let parameter: Parameter
#Environment(\.managedObjectContext) var managedObjectContext
#StateObject private var measurementStore = MeasurementStore()
#State private var displayMeasurementEntryView = false
#State private var selectedMeasurementIndex: Int?
private var measurementsRequest: FetchRequest<ParameterMeasurement>
private var measurements: FetchedResults<ParameterMeasurement> { measurementsRequest.wrappedValue }
private var measurementValues: [Double] { (measurements.map { $0.value }) }
private var measurementDeltas: [Double?] { measurementValues.deltasBetweenElements() }
private var measurementFormatter: MeasurementFormatter {
let formatter = MeasurementFormatter()
let numberFormatter = NumberFormatter()
numberFormatter.alwaysShowsDecimalSeparator = false
numberFormatter.maximumFractionDigits = 2
numberFormatter.numberStyle = .decimal
numberFormatter.usesGroupingSeparator = true
formatter.numberFormatter = numberFormatter
formatter.unitOptions = .providedUnit
formatter.unitStyle = .medium
return formatter
}
var body: some View {
List {
ForEach(measurements.indices, id: \.self) { index in
Button(action: {
selectedMeasurementIndex = index
displayMeasurementEntryView = true
}, label: {
HStack(content: {
VStack(alignment: .leading) {
Text(formattedMeasurement(at: index))
if index < measurements.count - 1 {
HStack(content: {
Image(systemName: deltaIconName(at: index))
Text(deltaString(for: index))
})
}
if let date = measurements[index].date {
FormattedDateTimeView(date: date)
}
}
})
})
}
.onDelete(perform: deleteMeasurements(at:))
}
.navigationTitle(parameter.name)
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(trailing: Button(action: {
displayMeasurementEntryView = true
}, label: {
Image(systemName: Icon.plusCircleFill)
}))
.sheet(isPresented: $displayMeasurementEntryView, onDismiss: {
selectedMeasurementIndex = nil
}) {
ParameterMeasurementEntryView(parameter: parameter, entryMode: entryMode())
.environment(\.managedObjectContext, managedObjectContext)
}
}
// MARK: Initialization
init(parameter: Parameter) {
self.parameter = parameter
let entity = ParameterMeasurement.entity()
let sortDescriptors = [NSSortDescriptor(key: #keyPath(ParameterMeasurement.date), ascending: false)]
let predicateFormat = "%K =[c] %#"
let predicateArguments = [#keyPath(ParameterMeasurement.parameterName), parameter.name]
let predicate = NSPredicate(format: predicateFormat, argumentArray: predicateArguments)
measurementsRequest = FetchRequest(entity: entity, sortDescriptors: sortDescriptors, predicate: predicate, animation: .none)
}
// MARK: Deletion
private func deleteMeasurements(at offsets: IndexSet) {
offsets.forEach { managedObjectContext.delete(measurements[$0]) }
PersistenceStack.saveContext()
}
// MARK: Helpers
private func formattedMeasurement(at index: Int) -> String {
let value = measurements[index].value
switch parameter.measurementUnit {
case .unitDispersion(units: _, defaultUnit: let unit):
let measurement = Measurement<Unit>(value: value, unit: unit)
return measurementFormatter.string(from: measurement)
}
}
private func deltaIconName(at index: Int) -> String {
guard let delta = measurementDeltas[index] else { fatalError("Expected delta") }
if delta == 0 { return Icon.arrowUpArrowDown }
return delta > 0 ? Icon.arrowUp : Icon.arrowDown
}
private func deltaString(for index: Int) -> String {
guard let delta = measurementDeltas[index] else { return "" }
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.maximumFractionDigits = 0
let absolute = abs(delta)
guard let formatted = formatter.string(from: absolute as NSNumber) else { fatalError("Expected formatted delta") }
return formatted
}
private func deltaBetweenMeasurement(at firstIndex: Int, and secondIndex: Int) -> Double {
measurementValues[firstIndex] - measurementValues[secondIndex]
}
private func entryMode() -> MeasurementEntryMode {
if let index = selectedMeasurementIndex {
return .edit(measurement: measurements[index])
}
return .add
}
}
Below, you can see the implementation of the view used to add/edit a measurement and persist it.
import SwiftUI
struct ParameterMeasurementEntryView: View {
// MARK: Properties
let parameter: Parameter
let entryMode: MeasurementEntryMode
#Environment(\.presentationMode) var presentationMode
#Environment(\.managedObjectContext) var managedObjectContext
#State private var measurementValueString = ""
private var measurementValue: Double? { Double(measurementValueString) }
private var cancelButton: some View {
Button(action: {
dismiss()
}, label: {
Text("Cancel")
})
}
private var saveButton: some View {
Button(action: {
saveNewMeasurement()
dismiss()
}, label: {
Text("Save")
})
.disabled(disableSaveButton())
}
var body: some View {
NavigationView(content: {
Form(content: {
Section(header: Text("Measurement")) {
HStack {
TextField("Value", text: $measurementValueString)
Text(defaultUnitSymbol())
}
}
})
.navigationTitle(parameter.name)
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(leading: cancelButton, trailing: saveButton)
.onAppear(perform: setMeasurmentTextIfEditingMeasurement)
})
}
// MARK: Initialization
init(parameter: Parameter, entryMode: MeasurementEntryMode) {
self.parameter = parameter
self.entryMode = entryMode
}
// MARK: Helpers
private func dismiss() {
presentationMode.wrappedValue.dismiss()
}
private func disableSaveButton() -> Bool {
let measurementIsInvalid = measurementValue == nil
if case let .edit(measurement) = entryMode {
let enteredValueEqualsCurrentValue = Double(measurementValueString) == measurement.value
return measurementIsInvalid || enteredValueEqualsCurrentValue
}
return measurementIsInvalid
}
private func saveNewMeasurement() {
guard let value = measurementValue else { return assertionFailure("Expected measurement value") }
let measurement = ParameterMeasurement(entity: ParameterMeasurement.entity(), insertInto: managedObjectContext)
measurement.value = value
measurement.date = Date()
measurement.parameterName = parameter.name
PersistenceStack.saveContext()
}
private func defaultUnitSymbol() -> String {
switch parameter.measurementUnit {
case .unitDispersion(_, defaultUnit: let defaultUnit): return defaultUnit.symbol
}
}
private func setMeasurmentTextIfEditingMeasurement() {
if case let .edit(measurment) = entryMode { measurementValueString = String(measurment.value) }
}
}
MeasurementEntryMode is a simple enum that allows the list to tell the add/entry view if it's adding a new measurement or editing an existing one.
import Foundation
enum MeasurementEntryMode {
// MARK: Cases
case add, edit(measurement: ParameterMeasurement)
}
What is causing the persisted measurement's value to not be displayed in the TextField on the first presentation of the add/edit view but be displayed on the second presentation?
Even the following trivial example yields the same results:
struct PrimaryView: View {
#State private var selectedIndex: Int?
#State private var showDetail = false
var body: some View {
NavigationView {
List {
ForEach(Array(0...50).indices, id: \.self) { index in
Button(action: {
selectedIndex = index
showDetail = true
}, label: {
Text("\(index)")
})
}
}
.sheet(isPresented: $showDetail, onDismiss: {
selectedIndex = nil
}) {
Text(String(describing: selectedIndex))
}
}
}
}
According to Justin Stanley's response to my question on Twitter, simply moving the code to set whether the detail view is shown from the Button's action to an View.onChange(of:perform:) modifier fixes the issue.