为什么只有在第二次显示视图后才用注入的对象填充SwiftUI视图

时间:2020-07-07 11:35:12

标签: swiftui

我有一个使用核心数据存储测量值的项目。用户可以添加要保留的新度量,也可以编辑保留的度量。

尝试编辑持久测量时出现我遇到的问题。选择一个持久测量后,将向用户显示视图以编辑和保存该测量。选定的度量从列表传递到显示的视图,在该视图中,值填充TextField。不幸的是,在应用程序中首次显示视图时,该值不会填充TextField。仅在第二次演示后,测量值才会填充TextField

用户可以显示视图以添加要保留的新度量,取消并关闭它,选择现有度量,并且该度量的值将显示在显示的TextField中。似乎用于添加/编辑度量的视图的初始表示不包含在第一个表示上选择的度量。只有在首次展示和撤消之后,该值才会填充TextField

下面,您会看到一个22秒的GIF,其中显示了当前行为。

GIF of Xcode Simulator replicating described issue

在GIF中,您可以看到已选择一个持久的度量,并且当前视图的TextField未填充度量的值。仅在第二个演示文稿中填充它。 GIF的后半部分显示了保存新度量的过程,并且在随后的演示中,TextField填充了该度量的值。

如果希望重现所描述的行为,则可以使用URL指向的feature/edit-measurement分支找到项目的存储库here

复制步骤

  1. 启动应用程序
  2. 从列表中选择任何参数
  3. 点击尾随导航按钮
  4. TextField
  5. 中输入一个值
  6. 点击“保存”按钮
  7. 返回到根视图(参数列表)
  8. 选择在第5步中保存测量值的参数
  9. 在列表中选择新近保留的测量值
  10. 通知未填充的TextField
  11. 点击“取消”按钮或向下拖动以取消视图
  12. 选择在第7步中选择的相同测量值
  13. 通知已填充的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))
            }
        }
    }
}

1 个答案:

答案 0 :(得分:0)

根据贾斯汀·斯坦利(Justin Stanley)在Twitter上的response to my question,只需移动代码以设置是否将详细视图从Button的操作显示到View.onChange(of:perform:)修饰符即可解决此问题。< / p>

Screenshot of Twitter response from Justin Stanley displaying image of code