У меня есть LandingView
, у которого есть viewModel, который является ObservableObject. LandingView динамически загружает некоторые CardView
и передает объект модели (свойство Binding).
CardView
далее вкладывает четыре разных подпредставления и передает необходимые атрибуты через привязку. В этих четырех подпредставлениях есть текстовые поля. Когда пользователь обновляет данные текстового поля, обновленные данные возвращаются в viewModel через Binding.
Теперь проблема в том, что когда пользователь вводит один символ в любом из этих текстовых полей, модель представления обновляется и, следовательно, LandingView перерисовывается, что приводит к тому, что текстовое поле теряет фокус, и пользователю приходится снова нажимать на текстовое поле. .
Есть ли способ это исправить? Я знаю, что могу это исправить, избавившись от свойств привязки и используя какой-то другой механизм для поддержания потока данных. Но можно ли это исправить в самой текущей настройке?
Заранее большое спасибо!!!!
struct LandingView: View {
@StateObject var viewModel = LandingViewModel(gridDataArray: [])
@State var name: String = ""
var body: some View {
let _ = Self._printChanges()
ZStack {
Color(red: 242/255, green: 242/255, blue: 242/255)
listView
.background(Color.white)
.padding()
}
}
private var listView: some View {
ScrollView {
VStack {
ForEach(Array(viewModel.gridDataArray.enumerated()), id: \.element) { index, element in
CardView(dataModel: $viewModel.gridDataArray[index])
.equatable()
}
}
}
}
}
struct CardView: View, Equatable {
static func == (lhs: CardView, rhs: CardView) -> Bool {
lhs.dataModel.hashValue == rhs.dataModel.hashValue
}
@Binding var dataModel: LandingViewDataModel
var body: some View {
VStack(spacing: 0) {
NavyBlueView(productName: $dataModel.productName,
productId: $dataModel.productId,
productLabel: dataModel.productLabel)
YellowView(weightedProductPrice: $dataModel.weightedProductPrice,
wacSubWACUnitPrice: $dataModel.wacSubWACUnitPrice,
pvpUnitPrice: $dataModel.pvpUnitPrice)
SkyBlueView(weightedPriceUsagePercent: $dataModel.weightedPriceUsagePercent,
wacSubWACUnitPricePercent: $dataModel.wacSubWACUnitPricePercent,
pvpUnitPricePercent: $dataModel.pvpUnitPricePercent,
totalPercent: $dataModel.totalPercent)
GreenView(weightedPriceCalculated: $dataModel.weightedPriceCalculated,
wacSubWACUnitPriceCalculated: $dataModel.wacSubWACUnitPriceCalculated,
pvpUnitPriceCalculated: $dataModel.pvpUnitPriceCalculated,
totalCalculated: $dataModel.totalCalculated)
}
}
}
struct NavyBlueView: View {
@Binding var productName: String
@Binding var productId: String?
var productLabel: String
var body: some View {
VStack(alignment: .leading) {
Text(productLabel)
.padding()
.padding(.bottom, -10)
.foregroundStyle(Color.white)
.frame(maxWidth: .infinity, alignment: .leading)
HStack {
Spacer().frame(width: 10)
TextField("Product Name", text: $productName)
.textFieldStyle(.plain)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.background(Rectangle().fill(Color.white))
.frame(width: 500, height: 35)
.padding(.leading, 15)
.padding(.bottom, 10)
HStack {
Text(" ")
.frame(maxWidth: .infinity)
Text("Weighted Price")
.frame(maxWidth: .infinity)
Text("WAC/SubWAC Unit Price")
.frame(maxWidth: .infinity)
Text("340B/PVP Unit Price")
.frame(maxWidth: .infinity)
Text("Total")
.frame(maxWidth: .infinity)
}
.foregroundStyle(Color.white)
.padding(.bottom, 10)
}
.background(Color(red: 41 / 255, green: 78 / 255, blue: 124 / 255))
.clipShape(.rect(
topLeadingRadius: 10,
bottomLeadingRadius: 0,
bottomTrailingRadius: 0,
topTrailingRadius: 10)
)
.padding(.horizontal)
}
}
struct YellowView: View {
@Binding var weightedProductPrice: String
@Binding var wacSubWACUnitPrice: String
@Binding var pvpUnitPrice: String
var body: some View {
HStack {
Group {
Text("Product Price")
HStack {
Spacer().frame(width: 10)
TextField("Product price", text: $weightedProductPrice)
.textFieldStyle(.plain)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.background(Rectangle().fill(Color.white))
.frame(height: 35)
HStack {
Spacer().frame(width: 10)
TextField("Product price", text: $wacSubWACUnitPrice)
.textFieldStyle(.plain)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.background(Rectangle().fill(Color.white))
.frame(height: 35)
.padding(10)
HStack {
Spacer().frame(width: 10)
TextField("Product price", text: $pvpUnitPrice)
.textFieldStyle(.plain)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.background(Rectangle().fill(Color.white))
.frame(height: 35)
Text("")
.frame(maxWidth: .infinity)
.frame(height: 35)
.padding(10)
.background(Color(red: 238/255, green: 238/255, blue: 239/255))
}
.frame(maxWidth: .infinity)
}
.frame(maxWidth: .infinity)
.background(Color(red: 255/255, green: 254/255, blue: 185/255))
.padding(.horizontal)
}
}
struct SkyBlueView: View {
@Binding var weightedPriceUsagePercent: String
@Binding var wacSubWACUnitPricePercent: String
@Binding var pvpUnitPricePercent: String
@Binding var totalPercent: String
var body: some View {
HStack {
Group {
Text("Usage %")
HStack {
Spacer().frame(width: 10)
TextField("Percentage", text: $weightedPriceUsagePercent)
.textFieldStyle(.plain)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.background(Rectangle().fill(Color.white))
.frame(height: 35)
HStack {
Spacer().frame(width: 10)
TextField("Percentage", text: $wacSubWACUnitPricePercent)
.textFieldStyle(.plain)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.background(Rectangle().fill(Color.white))
.frame(height: 35)
.padding(10)
HStack {
Spacer().frame(width: 10)
TextField("Percentage", text: $pvpUnitPricePercent)
.textFieldStyle(.plain)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.background(Rectangle().fill(Color.white))
.frame(height: 35)
ZStack {
HStack {
Spacer().frame(width: 10)
TextField("", text: $totalPercent)
.textFieldStyle(.plain)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.background(Rectangle().fill(Color.white))
.frame(height: 35)
.border((Color(red: 183/255, green: 181/255, blue: 182/255)), width: 2)
.padding(10)
}
.frame(maxWidth: .infinity)
.frame(height: 35)
.padding(10)
.background(Color(red: 238/255, green: 238/255, blue: 239/255))
}
.frame(maxWidth: .infinity)
}
.frame(maxWidth: .infinity)
.background(Color(red: 199/255, green: 229/255, blue: 243/255))
.padding(.horizontal)
}
}
struct GreenView: View {
@Binding var weightedPriceCalculated: String
@Binding var wacSubWACUnitPriceCalculated: String
@Binding var pvpUnitPriceCalculated: String
@Binding var totalCalculated: String
var body: some View {
HStack {
Group {
Text("Weighted Price")
.frame(maxWidth: .infinity)
Text(weightedPriceCalculated)
.frame(maxWidth: .infinity)
Text(wacSubWACUnitPriceCalculated)
.frame(maxWidth: .infinity)
Text(pvpUnitPriceCalculated)
.frame(maxWidth: .infinity)
ZStack {
Text(totalCalculated)
.frame(maxWidth: .infinity)
}
.frame(maxWidth: .infinity)
.frame(height: 25)
.padding(10)
.background(Color(red: 76/255, green: 174/255, blue: 234/255))
}
.frame(maxWidth: .infinity)
}
.frame(maxWidth: .infinity)
.background(Color(red: 195/255, green: 231/255, blue: 145/255))
.padding(.horizontal)
}
}
final class LandingViewModel: ObservableObject {
@Published var gridDataArray: [LandingViewDataModel] = []
init(gridDataArray: [LandingViewDataModel]) {
self.gridDataArray = gridDataArray
self.gridDataArray.append(LandingViewDataModel(productLabel: "Endo Product",
productName: "Endo product something",
productId: "Endo product ID",
weightedProductPrice: "122.43",
weightedPriceUsagePercent: "30",
weightedPriceCalculated: "",
wacSubWACUnitPrice: "111.33",
wacSubWACUnitPricePercent: "20",
wacSubWACUnitPriceCalculated: "",
pvpUnitPrice: "222.33",
pvpUnitPricePercent: "50",
pvpUnitPriceCalculated: "",
totalPercent: "",
totalCalculated: ""))
self.gridDataArray.append(LandingViewDataModel(productLabel: "Competitor 1",
productName: "Competitor 1",
productId: nil,
weightedProductPrice: "198.43",
weightedPriceUsagePercent: "10",
weightedPriceCalculated: "",
wacSubWACUnitPrice: "987.33",
wacSubWACUnitPricePercent: "70",
wacSubWACUnitPriceCalculated: "",
pvpUnitPrice: "876.33",
pvpUnitPricePercent: "20",
pvpUnitPriceCalculated: "",
totalPercent: "",
totalCalculated: ""))
self.gridDataArray.append(LandingViewDataModel(productLabel: "Competitor 2",
productName: "Competitor 2",
productId: nil,
weightedProductPrice: "445.43",
weightedPriceUsagePercent: "40",
weightedPriceCalculated: "",
wacSubWACUnitPrice: "432.33",
wacSubWACUnitPricePercent: "20",
wacSubWACUnitPriceCalculated: "",
pvpUnitPrice: "456.33",
pvpUnitPricePercent: "40",
pvpUnitPriceCalculated: "",
totalPercent: "",
totalCalculated: ""))
}
}
struct LandingViewDataModel: Hashable, Equatable {
let productLabel: String
var productName: String
var productId: String?
var weightedProductPrice: String {
didSet {
calculate()
}
}
var weightedPriceUsagePercent: String {
didSet {
calculate()
}
}
var weightedPriceCalculated: String
var wacSubWACUnitPrice: String {
didSet {
calculate()
}
}
var wacSubWACUnitPricePercent: String {
didSet {
calculate()
}
}
var wacSubWACUnitPriceCalculated: String
var pvpUnitPrice: String {
didSet {
calculate()
}
}
var pvpUnitPricePercent: String {
didSet {
calculate()
}
}
var pvpUnitPriceCalculated: String
var totalPercent: String
var totalCalculated: String
}
extension LandingViewDataModel {
private mutating func calculate() {
if let flotPrice = Float(weightedProductPrice), let percentage = Float(weightedPriceUsagePercent) {
weightedPriceCalculated = "\(flotPrice * (percentage / 100))"
}
if let flotPrice = Float(wacSubWACUnitPrice), let percentage = Float(wacSubWACUnitPricePercent) {
wacSubWACUnitPriceCalculated = "\(flotPrice * (percentage / 100))"
}
if let flotPrice = Float(pvpUnitPrice), let percentage = Float(pvpUnitPricePercent) {
pvpUnitPriceCalculated = "\(flotPrice * (percentage / 100))"
}
if let weightedPriceUsagePercent = Int(weightedPriceUsagePercent),
let wacSubWACUnitPricePercent = Int(wacSubWACUnitPricePercent),
let pvpUnitPricePercent = Int(pvpUnitPricePercent) {
totalPercent = "\(weightedPriceUsagePercent + wacSubWACUnitPricePercent + pvpUnitPricePercent)"
}
if let weightedPriceCalculated = Float(weightedPriceCalculated),
let wacSubWACUnitPriceCalculated = Float(wacSubWACUnitPriceCalculated),
let pvpUnitPriceCalculated = Float(pvpUnitPriceCalculated) {
totalCalculated = "\(weightedPriceCalculated + wacSubWACUnitPriceCalculated + pvpUnitPriceCalculated)"
}
}
}
Я обновил подпредставления, содержащие текстовые поля, которые я изначально не включил, чтобы уменьшить размер вопроса. @workingdogsupportUkraine Я не использую .onChange. Согласен с комментарием EquatableView. Я просто экспериментировал со многими вещами.
Я уже пробовал удалить свойство @Binding из CardView, и это устраняет проблему потери фокуса текстового поля. Но на самом деле мне этого не хочется, поскольку моя модель представления не будет обновляться последними данными из текстовых полей.
Попробуйте создать LandingViewDataModel
class
вместо структуры. Вы используете изменяющую функцию в своей структуре, а это означает, что вы постоянно создаете новые ее экземпляры.
Кто бы ни проголосовал против вопроса, просьба указать причину в комментариях!
ForEach(Array( Перечисленная распространенная ошибка могла привести к отрицательному голосованию
Здесь я согласен с @malhal. В результате ввод символа приводит к тому, что CardView меняет свою идентичность. И это заставляет его полностью сбрасывать свою «память» и строить с нуля, что убирает фокус. Личное примечание: отрицательный голос никоим образом не полезен и не оправдан. Эта ошибка незначительна и требует глубоких знаний о том, как работает система SwiftUI.
@malhal Спасибо. Вы указываете мне правильное направление!! Какова альтернатива перечислению, если мне нужен индекс?
Чтобы решить эту проблему, вам придется внести немало изменений в исходный дизайн.
Убедитесь, что при использовании ForEach
, List
и т. д. тип элемента (он же LandingViewDataModel
в переданном контейнере произвольного доступа соответствует Identifiable
.
Избегайте прямого изменения состояния представления (т. е. функции изменения LandingViewDataModel
). Вместо этого переместите логику в ObservableObject (он же calculate()
).
Эта точка зрения должна быть «функцией государства». Утройте это.
Представление никогда не должно менять свое состояние представления непосредственно в «источнике истины», т. е. в вашей модели представления (то есть не используйте двусторонние привязки).
Из №2, №3 и №4 следует: при разработке представлений SwiftUI избегайте использования двусторонних привязок без крайней необходимости. Вместо этого используйте комбинацию «Let Values», чтобы определить состояние представления в дополнение к переменным закрытия, которые определяют «команды» (намерения), которые могут быть отправлены из представления в ObservableObject, который обрабатывает эти команды. Таким образом, вместо передачи LandingViewModel
передайте значение const (которое представляет «состояние представления») и замыкания, которые вызываются, когда происходит событие, инициированное пользователем (т. е. изменение текста EditField, нажатие кнопки и т. д.). ).
Чтобы SwiftUI мог выполнять оптимальное сравнение, избегайте использования функций, которые возвращают Binding
изнутри тела. Вместо этого используйте представление SwiftUI. Лучше всего создавать много меньших представлений вместо нескольких больших представлений.
Кончик:
Соберите и перечислите все события, которые могут произойти в вашем поле зрения. Затем создайте перечисление:
typealias ID = LandingViewDataModel.ID
enum Event {
case textFieldXyzChanged(id: ID, text: String)
case focusChanged(id: ID)
case submitButtonTapped(id: ID, payload: Data)
...
}
(примечание: поскольку ваши события связаны с определенным элементом в массиве модели представления, вам необходимо отправить идентификатор элемента вместе с событием, чтобы модель представления знала, с каким элементом он связан)
Тогда вам понадобится только один метод в ObservableObject:
send(_ event: Event)
который способен обрабатывать все события, которые могут произойти в этом варианте использования. Таким образом, вам нужно передать только одну функцию закрытия вашим представлениям и подпредставлениям.
Большое спасибо! Я очень ценю подробный описательный ответ и ценные советы. На самом деле я рассматривал этот подход (в некоторой степени перспективный), но я хотел проверить, есть ли способ решить эту проблему с помощью самого @Binding. Я попробую.
В вашем случае вы не можете полностью избежать привязок SwiftUI, поскольку этого требует TextFields. Но на уровне корневого представления вам следует избегать их и передавать модель как константу let и замыкание, вызывающее viewModel.send(.someEvent)
. Кроме того, вам необходимо передавать идентификатор в каждом событии, который является идентификатором текущего элемента в массиве, с которым связано событие: enum Event { case doSomething(id: UUID) }
Да! Я на этом.
Я исправил это согласно вашему последнему комментарию. Теперь все хорошо. Удалена оболочка @Published из массива модели представления и удалена привязка с корневого уровня. Вместо этого просто передайте простой объект модели в CardView. Я отметил это как принятый ответ. Возможно, я добавлю свои изменения в отдельный ответ, просто для краткости.
@RashmiRanjanMallick Конечно, опубликуйте улучшенную версию. ;)
Now the problem is that the when a user types a single character in any of those text fields...
, в вашем коде нетTextField
, где вы говорите, что у вас есть проблема. Я подозреваю, что вы используете.onChange
вместо.onSubmit
. Покажите код, в котором естьTextField
. Обратите внимание: уберитеstatic func == ...
, просмотры уже соответствуютEquatableView
.