Индекс вне допустимого диапазона при обновлении @Published var

Я новичок в Swift/SwiftUI и пытаюсь создать приложение, которое работает с API Trello.

Существует класс «TrelloApi», который доступен как @EnvironmentObject во всем приложении. Этот же класс также используется для вызовов API.

За раз просматривается одна доска. На доске много списков, и в каждом списке много карточек.

Теперь у меня проблема с рендерингом: всякий раз, когда я переключаю доски, и в любом списке на новой доске меньше карт, чем раньше, я получаю следующую ошибку в обработчике onReceive, где мне нужно выполнить некоторые проверки, чтобы обновить внешний вид карт:

Swift/ContiguousArrayBuffer.swift:575: Fatal error: Index out of range
2022-10-19 09:04:11.319982+0200 trello[97617:17713580] Swift/ContiguousArrayBuffer.swift:575: Fatal error: Index out of range

Модели

struct BoardPrefs: Codable {
    var backgroundImage: String? = "";
}

struct BasicBoard: Identifiable, Codable {
    var id: String;
    var name: String;
    var prefs: BoardPrefs;
}

struct Board: Identifiable, Codable {
    var id: String;
    var name: String;
    var prefs: BoardPrefs;
    
    var lists: [List] = [];
    var cards: [Card] = [];
    var labels: [Label] = [];
    
}

struct List: Identifiable, Codable, Hashable {
    var id: String;
    var name: String;
    var cards: [Card] = [];
    
    private enum CodingKeys: String, CodingKey {
        case id
        case name
    }
}

struct Card: Identifiable, Codable, Hashable {
    var id: String;
    var idList: String = "";
    var labels: [Label] = [];
    var idLabels: [String] = [];
    var name: String;
    var desc: String = "";
    var due: String?;
    var dueComplete: Bool = false;
}

TrelloApi.swift (вызов HTTP удален для простоты)

class TrelloApi: ObservableObject {
    let key: String;
    let token: String;
    
    @Published var board: Board;
    @Published var boards: [BasicBoard];
    
    init(key: String, token: String) {
        self.key = key
        self.token = token
        self.board = Board(id: "", name: "", prefs: BoardPrefs())
        self.boards = []
    }
    
    func getBoard(id: String, completion: @escaping (Board) -> Void = { board in }) {
        if id == "board-1" {
            self.board = Board(id: "board-1", name: "board-1", prefs: BoardPrefs(), lists: [
                List(id: "board-1-list-1", name: "board-1-list-1", cards: [
                    Card(id: "b1-l1-card1", name: "b1-l1-card1"),
                ]),
                List(id: "board-1-list-2", name: "board-1-list-2", cards: [
                    Card(id: "b1-l2-card1", name: "b1-l2-card1"),
                    Card(id: "b1-l2-card2", name: "b1-l2-card2"),
                ])
            ])
            
            completion(self.board)
        } else {
            self.board = Board(id: "board-2", name: "board-2", prefs: BoardPrefs(), lists: [
                List(id: "board-2-list-1", name: "board-2-list-1", cards: [
                ]),
                List(id: "board-2-list-2", name: "board-2-list-2", cards: [
                    Card(id: "b2-l2-card1", name: "b2-l2-card1"),
                ])
            ])
            
            completion(self.board)
        }
    }
}

ContentView.swift

struct ContentView: View {
    @EnvironmentObject var trelloApi: TrelloApi;
    
    var body: some View {
        HStack {
            VStack {
                Text("Switch Board")
                Button(action: {
                    trelloApi.getBoard(id: "board-1")
                }) {
                    Text("board 1")
                }
                Button(action: {
                    trelloApi.getBoard(id: "board-2")
                }) {
                    Text("board 2")
                }
            }
            VStack {
                ScrollView([.horizontal]) {
                    ScrollView([.vertical]) {
                        VStack(){
                            HStack(alignment: .top) {
                                ForEach($trelloApi.board.lists) { list in
                                    TrelloListView(list: list)
                                        .fixedSize(horizontal: false, vertical: true)
                                }
                            }
                            .padding()
                            .frame(maxHeight: .infinity, alignment: .top)
                        }
                    }
                }
            }
        }.onAppear {
            trelloApi.getBoard(id: "board-1")
        }
        .frame(minWidth: 900, minHeight: 600, alignment: .top)
    }
}

TrelloListView.swift

struct TrelloListView: View {
    @EnvironmentObject var trelloApi: TrelloApi;
    @Binding var list: List;
    
    var body: some View {
        VStack() {
            Text(self.list.name)
            Divider()
            SwiftUI.List(self.$list.cards, id: \.id) { card in
                CardView(card: card)
                
            }
            .listStyle(.plain)
            .frame(minHeight: 200)
        }
        .padding(4)
        .cornerRadius(8)
        .frame(minWidth: 200)
    }
}

CardView.swift

struct CardView: View {
    @EnvironmentObject var trelloApi: TrelloApi;
    
    @Binding var card: Card;
    
    var body: some View {
        VStack(alignment: .leading) {
            HStack {
                VStack(alignment: .leading, spacing: 0) {
                    Text(card.name)
                        .bold()
                        .font(.system(size: 14))
                        .multilineTextAlignment(.leading)
                        .lineLimit(1)
                        .foregroundColor(.white)
                    
                    Text(card.desc)
                        .lineLimit(1)
                        .foregroundColor(.secondary)
                }.padding()
                Spacer()
            }
        }
        .frame(alignment: .leading)
        .onReceive(Just(card)) { newCard in
            // CRASH LOCATION: "Index out of range" for self.card.labels
            if self.card.labels != newCard.labels {
                print("(check if card color should change based on labels)")
            }
        }
        .cornerRadius(4)
    }
}

Я выделил место крушения комментарием. Я не передаю никаких индексов в ForEach или List и перезаписываю весь объект trelloApi.board, поэтому я не уверен, почему я получаю эту ошибку.

Вместо этого я попытался использовать ForEach внутри SwiftUI.List, но это также ничего не меняет.

Минимальный воспроизводимый код также можно найти в моем репозитории GitHub: https://github.com/Rukenshia/trello/tree/troubleshooting-board-switch-crash/trello

Вы должны проверить, является ли card.labels пустым или нет. А потом исходить оттуда.

Lawrence Gimenez 19.10.2022 09:46

@LawrenceGimenez, похоже, это ничего не меняет - XCode выделяет исключение на «card.labels», поэтому, если я добавлю проверку для if card.labels.count > 0 { ... }, оно все равно выдает то же исключение.

Jan 19.10.2022 09:53

Почему вы вообще используете здесь onReceive, разве недостаточно того, что card объявлен как Binding?

Joakim Danielson 19.10.2022 09:57

@JoakimDanielson Я удалил большую часть кода для простого воспроизводимого примера — я использую это в реальном коде для обновления цвета фона карточек в зависимости от меток: github.com/Rukenshia/trello/blob/main/trello/views/…

Jan 19.10.2022 10:02

Да, я знаю, что это ничего не меняет, я пытаюсь шаг за шагом помочь вам в отладке. Спасибо за ответ. Обычно я не использую onReceive, но использую task.

Lawrence Gimenez 19.10.2022 10:09

Ценю твою помощь! Первоначально я использовал onReceive, потому что карточка могла меняться в фоновом режиме (например, когда запускается автоматизация Trello), поэтому я подумал, что onReceive будет полезен при изменении карточки. Могу ли я добиться этого с помощью .task? Я не слышал и не использовал его раньше и только что просмотрел документацию, но я не уверен, как я мог бы использовать его, чтобы реагировать на изменения в привязке.

Jan 19.10.2022 12:34

что такое Label (в карточке), это ваша собственная структура или представление SwiftUI Label? Если это ваша собственная структура, измените имя, оно может мешать работе SwiftUI Label. Аналогично для List. P.S. Все ; можете удалить, это из далекого прошлого.

workingdog support Ukraine 20.10.2022 07:09

Метка @workingdogsupportUkraine — это моя собственная структура, хороший совет. Я обновил названия всех из них, но, к сожалению, ничего не изменил. Также спасибо за подсказку о точках с запятой, не знал этого!

Jan 20.10.2022 09:52

Тот .onReceive(), который вы используете с издателем Just, созданным встроенным в вызове функции и который наблюдает за привязкой, очень подозрительный. По определению вид уже перерисовывается при изменении значения @Binding. В вашем коде слишком много состояний, попробуйте провести рефакторинг в меньшие представления и использовать ObservableObjects для моделирования состояния представления (например, CardView).

Louis Lac 21.10.2022 15:01

Кроме того, ваш код разбросан антипаттернами, такими как @State private var isHovering: Bool. @State свойства всегда должны иметь начальное значение и не должны устанавливаться вручную в представлении init. Если вам нужно обновить значение, как только появится представление, используйте модификатор .onAppear() или .task().

Louis Lac 21.10.2022 15:03

@LouisLac спасибо! Это многое прояснило. Я только что начал его рефакторинг, и теперь все в основном переработано. Анти-паттерны, о которых вы говорите, есть ли где-нибудь исчерпывающий список, чтобы я мог узнать о нем больше? Я думаю, что также не могу пометить ваш комментарий как ответ — не могли бы вы опубликовать его, чтобы я мог наградить вас наградой?

Jan 21.10.2022 15:48

@Jan Я написал подробный ответ. К сожалению, у меня нет хороших постов со списком антишаблонов, но я рекомендую документацию Apple SwiftUI и бесплатные учебные пособия!

Louis Lac 21.10.2022 17:05

@Jan Я думаю, что решил проблему сбоя. Взгляните на мой ответ, я обновил его решением.

Louis Lac 22.10.2022 17:30
Стоит ли изучать 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 называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
13
130
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

Точную проблему трудно отследить, но вот некоторые наблюдения и рекомендации.

Модификатор .onReceive(), который вы используете, выглядит подозрительно, потому что вы сами инициализируете издателя в вызове функции. Обычно вы используете .onReceive(), чтобы реагировать на события, опубликованные издателями, настроенными другим фрагментом кода.

Более того, вы используете этот .onReceive(), чтобы реагировать на изменения в свойстве @Binding, что является избыточным, поскольку по определению @Binding уже запускает обновление представления при изменении его значения.


РЕДАКТИРОВАТЬ

Кажется, это проблема, которая вызывает сбой в вашем приложении. Изменение .onReceive() на .onChange(), похоже, решает проблему:

.onChange(of: card) { newCard in
  if self.card.labels != newCard.labels {
    print("(check if card color should change based on labels)")
  }
}

Вы также, кажется, дублируете некоторое состояние:

.onReceive(Just(card)) { newCard in
  self.due = newCard.dueDate
}

Здесь вы продублировали дату выполнения, одна копия в self.due и другая копия в self.card.dueDate. В SwiftUI должен быть только один источник правды, и для вас это будет свойство card. Вы продублировали состояние в init: self.due = card.wrappedValue.dueDate. Доступ к .wrappedValue@Binding/State — это запах кода и признак того, что вы делаете что-то не так.

Наконец, вы используете анти-шаблон, который может быть опасен:

struct CardView: View {
  @State private var isHovering: Bool
  
  func init(isHovering: String) {
    self._isHovering = State(initialValue: false)
  }

  var body: some View { 
    ...
  }
}

Вам следует избегать инициализации оболочки свойства @State самостоятельно в представлении init. Свойство @State должно быть инициализировано в строке:

struct CardView: View {
  @State private var isHovering: Bool = false

  var body: some View { 
    ...
  }
}

Если по какой-то причине вам нужно настроить значение свойства @State, вы можете использовать модификатор представления .onAppear() или более новый .task(), чтобы изменить его значение после создания представления:

struct CardView: View {
  @State private var isHovering: Bool = false

  var body: some View { 
    SomeView()
      .onAppear {
        isHovering = Bool.random()
      }
  }
}

В качестве общего совета вы должны разбить свои взгляды на более мелкие части. Когда представление зависит от многих @State свойств и имеет множество .onChange() или .onReceive(), это обычно указывает на то, что пришло время переместить всю логику внутрь и ObservableObject или выполнить рефакторинг на более мелкие компоненты.

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