Как запретить перерисовку представления SwiftUI каждый раз при обновлении опубликованного свойства?

У меня есть 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)"
        }
    }
}

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.
workingdog support Ukraine 27.08.2024 01:31

Я обновил подпредставления, содержащие текстовые поля, которые я изначально не включил, чтобы уменьшить размер вопроса. @workingdogsupportUkraine Я не использую .onChange. Согласен с комментарием EquatableView. Я просто экспериментировал со многими вещами.

Rashmi Ranjan Mallick 27.08.2024 01:38

Я уже пробовал удалить свойство @Binding из CardView, и это устраняет проблему потери фокуса текстового поля. Но на самом деле мне этого не хочется, поскольку моя модель представления не будет обновляться последними данными из текстовых полей.

Rashmi Ranjan Mallick 27.08.2024 01:43

Попробуйте создать LandingViewDataModelclass вместо структуры. Вы используете изменяющую функцию в своей структуре, а это означает, что вы постоянно создаете новые ее экземпляры.

Paulw11 27.08.2024 03:39

Кто бы ни проголосовал против вопроса, просьба указать причину в комментариях!

Rashmi Ranjan Mallick 27.08.2024 08:41

ForEach(Array( Перечисленная распространенная ошибка могла привести к отрицательному голосованию

malhal 27.08.2024 10:22

Здесь я согласен с @malhal. В результате ввод символа приводит к тому, что CardView меняет свою идентичность. И это заставляет его полностью сбрасывать свою «память» и строить с нуля, что убирает фокус. Личное примечание: отрицательный голос никоим образом не полезен и не оправдан. Эта ошибка незначительна и требует глубоких знаний о том, как работает система SwiftUI.

CouchDeveloper 27.08.2024 10:48

@malhal Спасибо. Вы указываете мне правильное направление!! Какова альтернатива перечислению, если мне нужен индекс?

Rashmi Ranjan Mallick 27.08.2024 11:42
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
8
55
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Чтобы решить эту проблему, вам придется внести немало изменений в исходный дизайн.

  1. Убедитесь, что при использовании ForEach, List и т. д. тип элемента (он же LandingViewDataModel в переданном контейнере произвольного доступа соответствует Identifiable.

  2. Избегайте прямого изменения состояния представления (т. е. функции изменения LandingViewDataModel). Вместо этого переместите логику в ObservableObject (он же calculate()).

  3. Эта точка зрения должна быть «функцией государства». Утройте это.

  4. Представление никогда не должно менять свое состояние представления непосредственно в «источнике истины», т. е. в вашей модели представления (то есть не используйте двусторонние привязки).

  5. Из №2, №3 и №4 следует: при разработке представлений SwiftUI избегайте использования двусторонних привязок без крайней необходимости. Вместо этого используйте комбинацию «Let Values», чтобы определить состояние представления в дополнение к переменным закрытия, которые определяют «команды» (намерения), которые могут быть отправлены из представления в ObservableObject, который обрабатывает эти команды. Таким образом, вместо передачи LandingViewModel передайте значение const (которое представляет «состояние представления») и замыкания, которые вызываются, когда происходит событие, инициированное пользователем (т. е. изменение текста EditField, нажатие кнопки и т. д.). ).

  6. Чтобы 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. Я попробую.

Rashmi Ranjan Mallick 27.08.2024 11:53

В вашем случае вы не можете полностью избежать привязок SwiftUI, поскольку этого требует TextFields. Но на уровне корневого представления вам следует избегать их и передавать модель как константу let и замыкание, вызывающее viewModel.send(.someEvent). Кроме того, вам необходимо передавать идентификатор в каждом событии, который является идентификатором текущего элемента в массиве, с которым связано событие: enum Event { case doSomething(id: UUID) }

CouchDeveloper 27.08.2024 12:01

Да! Я на этом.

Rashmi Ranjan Mallick 27.08.2024 12:06

Я исправил это согласно вашему последнему комментарию. Теперь все хорошо. Удалена оболочка @Published из массива модели представления и удалена привязка с корневого уровня. Вместо этого просто передайте простой объект модели в CardView. Я отметил это как принятый ответ. Возможно, я добавлю свои изменения в отдельный ответ, просто для краткости.

Rashmi Ranjan Mallick 27.08.2024 12:31

@RashmiRanjanMallick Конечно, опубликуйте улучшенную версию. ;)

CouchDeveloper 27.08.2024 13:46

Другие вопросы по теме