TextField format, integer limits and fractions not applied

Hi Apple-Team,

the format of a text field, in my case a percentage with one decimal place, is not applied when a button is pressed and the view is exited using dismiss. It is only used when another field receives focus. A button is not a focusable object, and this is a problem. This allows you to specify more decimal places, resulting in a saved value with unwanted decimal places.

It's even worse when you specify integer limits and the value exceeds the limit.

Do you always have to check the value before saving it? What's the point of having a format then?

Some example images: The items in the list have a fraction limit of 8.

The field has integer limit 2 and 1 fraction.

When focusing a different field the format is applied.

Here the code I used:


struct Item: Identifiable, Hashable {
    var id: Int
    var value: Double
}


struct WrongFractionsListView: View {
    @State private var items: [Item] = [
        Item(id: 1, value: 0.225),
        Item(id: 2, value: 0.377),
        Item(id: 3, value: 0.241)]
    
    enum ItemTransfer: Hashable {
        case new
    }
    
    var body: some View {
        List {
            ForEach(items) { item in
                HStack {
                    Text("\(item.id)")
                    Spacer()
                    Text("\(item.value, format: .percent.precision(.fractionLength(8)))")
                }
            }
        }
        .navigationDestination(for: ItemTransfer.self) { _ in
            WrongFractionsEditorView(items: $items)
        }
        .toolbar{
            ToolbarItem(placement: .topBarTrailing) {
                NavigationLink(value: ItemTransfer.new) {
                    Image(systemName: "plus")
                }
            }
        }
    }
}

#Preview {
    NavigationStack {
        WrongFractionsListView()
    }
}

struct WrongFractionsEditorView: View {
    @Environment(\.dismiss) var dismiss
    @State private var percentValue: Double = 0
    @State private var testValue: String = ""
    
    @Binding var items: [Item]
    
    var body: some View {
        Form {
            TextField("(%)", value: $percentValue, format: .percent.precision(.integerAndFractionLength(integerLimits: 0...2, fractionLimits: 0...1)))
                .keyboardType(.decimalPad)
            TextField("Just for Focus", text: $testValue)
        }
        .navigationTitle("Percent Fractions")
        .toolbar{
            ToolbarItem(placement: .topBarTrailing) {
                Button {
                    saveAndClose()
                } label: {
                    Image(systemName: "checkmark")
                }
            }
        }
    }
    
    private func saveAndClose() {
        let max = items.max { $0.id < $1.id}!.id
        let item: Item = .init(id: max+1, value: percentValue)
        items.append(item)
        dismiss()
    }
}

#Preview {
    @Previewable @State var items: [Item] = [
        Item(id: 1, value: 0.225),
        Item(id: 2, value: 0.377),
        Item(id: 3, value: 0.241)]
    NavigationStack {
        WrongFractionsEditorView(items: $items)
    }
}

How can I fix this?

Thank you Christian

I have a solution I've already thought of, but I'm not satisfied with it because I would have to implement it everywhere this problem occurs. At least the onChange() method in the View. Before posting the answer here, I was able to identify the bug more precisely. More about this at the end of the post.

Extension on Double for rounding:

extension Double {
    func rounded(toPlaces places: Int, rule: FloatingPointRoundingRule = .down) -> Double {
        let divisor = pow(10.0, Double(places))
        return (self * divisor).rounded(rule) / divisor
    }
}

A max percent value in the View:

private let maxPercent = 0.8

onChange of the value:

.onChange(of: percentValue) {
            checkPercentValue()
        }

The check function:

private func checkPercentValue() {
        let mf = modf(percentValue).1
        var val = mf.rounded(toPlaces: 3)
        if val > maxPercent {
            val = maxPercent
        }
        percentValue = val
    }

Not good, but it seems to work.

Yes, at first glance it seems to work, but when I enter values ​​above 100 it no longer works.

The function checkPercentValue() works correctly and sets percentValue to 0.0, but the field does not respond to this. The problem is:

.percent.precision(.integerAndFractionLength(integerLimits: 0...2, fractionLimits: 0...1))

Simply using .percent, however, works.

Christian

TextField format, integer limits and fractions not applied
 
 
Q