我有一个使用核心数据存储测量值的项目。用户可以添加要保留的新度量,也可以编辑保留的度量。
尝试编辑持久测量时出现我遇到的问题。选择一个持久测量后,将向用户显示视图以编辑和保存该测量。选定的度量从列表传递到显示的视图,在该视图中,值填充TextField
。不幸的是,在应用程序中首次显示视图时,该值不会填充TextField
。仅在第二次演示后,测量值才会填充TextField
。
用户可以显示视图以添加要保留的新度量,取消并关闭它,选择现有度量,并且该度量的值将显示在显示的TextField
中。似乎用于添加/编辑度量的视图的初始表示不包含在第一个表示上选择的度量。只有在首次展示和撤消之后,该值才会填充TextField
。
下面,您会看到一个22秒的GIF,其中显示了当前行为。
在GIF中,您可以看到已选择一个持久的度量,并且当前视图的TextField
未填充度量的值。仅在第二个演示文稿中填充它。 GIF的后半部分显示了保存新度量的过程,并且在随后的演示中,TextField
填充了该度量的值。
如果希望重现所描述的行为,则可以使用URL指向的feature/edit-measurement
分支找到项目的存储库here。
复制步骤
TextField
TextField
TextField
下面是显示持久测量的视图的实现:
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
}
}
在下面,您可以看到用于添加/编辑度量并将其保留的视图的实现。
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
是一个简单的enum
,它使列表可以告诉添加/进入视图是要添加新度量还是编辑现有度量。
import Foundation
enum MeasurementEntryMode {
// MARK: Cases
case add, edit(measurement: ParameterMeasurement)
}
是什么原因导致持久性测量的值未显示在添加/编辑视图的第一个演示文稿的TextField
上,而是显示在第二个演示文稿上?
即使下面的琐碎示例也得出相同的结果:
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))
}
}
}
}
答案 0 :(得分:0)
根据贾斯汀·斯坦利(Justin Stanley)在Twitter上的response to my question,只需移动代码以设置是否将详细视图从Button
的操作显示到View.onChange(of:perform:)
修饰符即可解决此问题。< / p>