更新@PUBLISHED变量时索引超出范围

ruarlubt  于 2022-10-23  发布在  Swift
关注(0)|答案(1)|浏览(145)

bounty 5天后到期。这个问题的答案有资格获得+200的声誉赏金。Jan想要引起更多的关注这个问题:解释为什么当前的方法不起作用并提供解决方案,或者数据的处理/传递是否应该以完全不同的方式设计。如果应该更改数据传递方式的体系结构,还请查看当前的GitHub repo,以了解TrelloApi:github.com/Rukenshia/trello的真正实现

我是Swift/SwiftUI的新手,我正在尝试构建一个与Trello API一起工作的应用程序。
在整个应用程序中有一个“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;
}

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)
    }
}

我已经用评论突出了坠机地点。我没有在ForEachList中传递任何索引,并且我覆盖了整个trelloApi.board对象,所以我不确定为什么会收到这个错误。
我尝试在SwiftUI.List中使用ForEach,但这也不会改变任何事情。
最小的可重现代码也可以在我的GitHub repo:https://github.com/Rukenshia/trello/tree/troubleshooting-board-switch-crash/trello上找到

xvw2m8pv

xvw2m8pv1#

确切的问题很难追踪,但这里有一些观察和建议。
您正在使用的.onReceive()修饰符看起来可疑,因为您自己在函数调用中内联地初始化了发布者。通常使用.onReceive()来响应由另一段代码设置的发布者发布的事件。
此外,您将使用此.onReceive()来对@Binding属性中的更改做出React,这是多余的,因为根据定义,@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属性。您复制了initself.due = card.wrappedValue.dueDate中的状态。访问@Binding/State.wrappedValue是一种代码气味,也是您做错了事情的标志。
最后,您使用了一个可能很危险的反模式:

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

  func init(isHovering: String) {
    self._isHovering = State(initialValue: false)
  }

  var body: some View { 
    ...
  }
}

您应该避免自己在视图的init中初始化@State属性 Package 。@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或重构到更小的组件中了。

相关问题