Предположим, у вас есть объект SwiftData, который не допускает дополнительных параметров:
@Model
class MySet: Identifiable {
var id: UUID
var weight: Int
var reps: Int
var isCompleted: Bool
var exercise: Exercise
Затем вы получаете из данных MySet список этих MySet и добавляете их в состояние:
@State var sets: [MySet]
if let newSets = exercise.sets { //unwrapping the passed exercises sets
if (!newSets.isEmpty) { //we passed actual sets in so set them to our set
sets = newSets
}
И вы используете этот список состояний в ForEach и привязываете текстовое поле непосредственно к данным следующим образом:
ForEach($sets){ $set in
TextField("\(set.weight)", value: $set.weight, formatter: NumberFormatter())
.keyboardType(.decimalPad)
TextField("\(set.reps)", value: $set.reps,formatter: NumberFormatter())
.keyboardType(.decimalPad)
}
Это свяжет все, что записано в TextField, непосредственно с объектом SwiftData, что является проблемой, поскольку, скажем, у вас записано 50, и вы хотите изменить его на 60, вам нужно иметь возможность очистить 50, чтобы записать 60, и поскольку объект SwiftData не разрешает ноль, он позволит вам удалить 0, но не 5. Есть ли способ исправить это, не разрешая объекту SwiftData дополнительные опции, например, перехватывать очистку TextField и делать его 0 вместо нуля, когда он очищается?
Я пытаюсь исправить это, привязав TextField к новому состоянию и используя onChanged:
@State private var reps: Int = 0
@State private var weight: Int = 0
TextField("\(myset.weight)", value: $weight, formatter: NumberFormatter())
.keyboardType(.decimalPad)
.onChange(of: weight) { newWeight in
// Code to run when propertyToWatch changes to newValue
if newWeight != 0 {
myset.weight = newWeight
}
}
но теперь он меняет каждое повторение и вес во всех подходах, когда я печатаю один, а не только тот, который я печатаю
Я пытаюсь это сделать, но теперь оно меняет каждое повторение и вес во всех подходах, когда я ввожу один
И это должно быть .onChange(of: weight)
без $.
Йоаким, я удалил последний вопрос, потому что я сузил точную причину проблемы, и я прошу вас не комментировать остальные мои вопросы, потому что вы еще не дали реального ответа и комментируете каждый вопрос для всех. делает. С уважением
Вы можете попробовать тот же метод, который описан в моем ответе, здесь: stackoverflow.com/questions/78802510/… Идея состоит в том, чтобы использовать text:
в TextField
, потому что текст затем постоянно обновляется. Но когда вы используете value:
, то есть «Для нестроковых типов оно обновляет значение, когда пользователь фиксирует свои изменения, например, нажимая клавишу Return». Таким образом, вы можете настроить логику в соответствии с вашими требованиями. См. текстовое поле.
Я не уверен, почему ваш код не работает, было бы полезно иметь минимальный воспроизводимый пример для работы.
Однако одним из способов решения проблемы было бы создание универсальной оболочки для TextField
. Обертка может хранить переменную теневого состояния и выполнять копирование из необязательного в необязательное.
Вот попытка реализовать такую обертку. Я выбрал инициализатор Textfield
, который принимает ParseableFormatStyle
:
struct ValueField<F>: View
where F: ParseableFormatStyle, F.FormatInput: Equatable, F.FormatOutput == String {
private let titleKey: LocalizedStringKey
@Binding private var value: F.FormatInput
private let format: F
private let prompt: Text?
@State private var fieldVal: F.FormatInput?
@FocusState private var isFocused: Bool
init(
_ titleKey: LocalizedStringKey,
value: Binding<F.FormatInput>,
format: F,
prompt: Text? = nil
) {
self.titleKey = titleKey
self._value = value
self.format = format
self.prompt = prompt
}
var body: some View {
TextField(titleKey, value: $fieldVal, format: format, prompt: prompt)
.focused($isFocused)
.onChange(of: fieldVal) { oldVal, newVal in
if let newVal, newVal != value {
value = newVal
}
}
.onChange(of: isFocused, initial: true) { oldVal, newVal in
if newVal {
fieldVal = nil
} else if fieldVal == nil {
fieldVal = value
}
}
}
}
Пример использования:
struct ContentView: View {
@State private var val: Int = 0
var body: some View {
VStack(spacing: 40) {
ValueField(
"Weight",
value: $val,
format: .number,
prompt: Text("Weight")
)
.keyboardType(.decimalPad)
TextField("Dummy", text: dummy)
}
.textFieldStyle(.roundedBorder)
.padding()
}
private var dummy: Binding<String> {
Binding<String>(
get: { "\(val)" },
set: { _ in /* NOP */ }
)
}
}
onChange
предназначен для внешних действий, это не лучший инструмент для преобразования состояния, для этого лучше подходят вычисляемые переменные
@malhal Я не понимаю, как любой из случаев .onChange
в ответе можно заменить вычисленными переменными и при этом работать так же (с немедленными обновлениями в ответ на действия). Не могли бы вы уточнить?
@malhal Я видел, как вы использовали вычисленную привязку в своем недавнем ответе, и вижу, как это можно применить к примеру использования, ответ обновлен. В примере второй TextField
использовался просто как способ переключения фокуса, так что это не имело большого значения. Но я до сих пор не понимаю, как даже вычисленную привязку можно использовать в структуре-оболочке. Пожалуйста, просветите меня.
Вычисляемая привязка предназначена для преобразования типов статистики для такого рода предельной длины. DidSet может быть лучше.
Вы можете создать собственную привязку, которая должна помочь:
private func weightBinding(for set: MySet) -> Binding<Int?> {
Binding<Int?>(
get: { set.weight },
set: { value in
if let value {
set.weight = value
}
}
)
}
ForEach(sets) { set in
TextField("\(set.weight)", value: weightBinding(for: set), formatter: NumberFormatter())
}
// or as extension if you prefer:
extension MySet {
var weightBinding: Binding<Int?> {
Binding<Int?>(
get: { self.weight },
set: { value in
if let value {
self.weight = value
}
}
)
}
}
TextField("\(set.weight)", value: set.weightBinding, formatter: NumberFormatter())
Таким образом вы можете очистить текстовое поле, но если вы не введете новое значение и не прекратите редактирование, оно будет использовать последнее значение.
Обновлено:
Если вы хотите установить значение 0, когда текстовое поле пусто, вам нужно изменить привязку на это:
Binding<Int?>(
get: { set.weight },
set: { value in
self.weight = value ?? 0
}
)
это почти работает, но если вес был 60, и я удаляю его и нажимаю на другое текстовое поле, последнее значение возвращается к 6 вместо 60
Нет, я имею в виду, что было 60, и я удалил 60 и ничего не вводил, затем я щелкнул по другому текстовому полю, и вместо возврата к 60 оно переходит к 6, поэтому эта реализация не работает
Как ты убрал 60? значение 60 -> удалить 0 -> значение 6 -> удалить 6 -> текстовое поле пусто -> оставить текстовое поле? Тогда это уже спасло бы 6
@johnsmith Я отредактировал свой ответ, установив значение 0, когда текстовое поле очищается.
Вы можете привязать
TextField
к другой переменной, которая может быть необязательной. Затем используйте обработчик.onChange
для обновления целевого значения при каждом изменении связанного значения.