Я новичок в 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
@LawrenceGimenez, похоже, это ничего не меняет - XCode выделяет исключение на «card.labels», поэтому, если я добавлю проверку для if card.labels.count > 0 { ... }
, оно все равно выдает то же исключение.
Почему вы вообще используете здесь onReceive, разве недостаточно того, что card
объявлен как Binding?
@JoakimDanielson Я удалил большую часть кода для простого воспроизводимого примера — я использую это в реальном коде для обновления цвета фона карточек в зависимости от меток: github.com/Rukenshia/trello/blob/main/trello/views/…
Да, я знаю, что это ничего не меняет, я пытаюсь шаг за шагом помочь вам в отладке. Спасибо за ответ. Обычно я не использую onReceive, но использую task.
Ценю твою помощь! Первоначально я использовал onReceive, потому что карточка могла меняться в фоновом режиме (например, когда запускается автоматизация Trello), поэтому я подумал, что onReceive будет полезен при изменении карточки. Могу ли я добиться этого с помощью .task
? Я не слышал и не использовал его раньше и только что просмотрел документацию, но я не уверен, как я мог бы использовать его, чтобы реагировать на изменения в привязке.
что такое Label
(в карточке), это ваша собственная структура или представление SwiftUI Label
? Если это ваша собственная структура, измените имя, оно может мешать работе SwiftUI Label
. Аналогично для List
. P.S. Все ;
можете удалить, это из далекого прошлого.
Метка @workingdogsupportUkraine — это моя собственная структура, хороший совет. Я обновил названия всех из них, но, к сожалению, ничего не изменил. Также спасибо за подсказку о точках с запятой, не знал этого!
Тот .onReceive()
, который вы используете с издателем Just
, созданным встроенным в вызове функции и который наблюдает за привязкой, очень подозрительный. По определению вид уже перерисовывается при изменении значения @Binding
. В вашем коде слишком много состояний, попробуйте провести рефакторинг в меньшие представления и использовать ObservableObjects
для моделирования состояния представления (например, CardView
).
Кроме того, ваш код разбросан антипаттернами, такими как @State private var isHovering: Bool
. @State
свойства всегда должны иметь начальное значение и не должны устанавливаться вручную в представлении init
. Если вам нужно обновить значение, как только появится представление, используйте модификатор .onAppear()
или .task()
.
@LouisLac спасибо! Это многое прояснило. Я только что начал его рефакторинг, и теперь все в основном переработано. Анти-паттерны, о которых вы говорите, есть ли где-нибудь исчерпывающий список, чтобы я мог узнать о нем больше? Я думаю, что также не могу пометить ваш комментарий как ответ — не могли бы вы опубликовать его, чтобы я мог наградить вас наградой?
@Jan Я написал подробный ответ. К сожалению, у меня нет хороших постов со списком антишаблонов, но я рекомендую документацию Apple SwiftUI и бесплатные учебные пособия!
@Jan Я думаю, что решил проблему сбоя. Взгляните на мой ответ, я обновил его решением.
Точную проблему трудно отследить, но вот некоторые наблюдения и рекомендации.
Модификатор .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
или выполнить рефакторинг на более мелкие компоненты.
Вы должны проверить, является ли card.labels пустым или нет. А потом исходить оттуда.