SwiftUI:从显示滑动按钮的旧列表切换到新列表时,UI 不正确

ray*_*ayx 5 list swipe ios swiftui

可以使用下面的代码一致地重现该问题。Xcode 13.3 + iOS 15.4(都是最新的)。

enum ListID: String, CaseIterable, Hashable, Identifiable {
    case list1 = "List1"
    case list2 = "List2"
    
    var id: Self {
        self
    }
}

struct ContentView: View {
    @State var listID: ListID = .list1
    
    var body: some View {
        VStack {
            // 1) Picker
            Picker(selection: $listID) {
                ForEach(ListID.allCases) { id in
                    Text(id.rawValue)
                }
            } label: {
                Text("Select a list")
            }
            .pickerStyle(.segmented)
            // 2) List
            switch listID {
            case .list1:
                createList(Array(1...2), id: .list1)
            case .list2:
                createList(Array(101...102), id: .list2)
            }
        }

    }
    
    @ViewBuilder func createList(_ itemValues: [Int], id: ListID) -> some View {
        List {
            ForEach(itemValues, id:\.self) { value in
                Text("\(value)")
                    .swipeActions(edge: .trailing, allowsFullSwipe: false) {
                        Button("Edit") {
                            // do nothing
                        }
                        .tint(.blue)
                    }
            }
        }
        .id(id)
    }
}
Run Code Online (Sandbox Code Playgroud)

重现问题的步骤:

  1. 启动应用程序。见图1。
  2. 滑动列表 1 中的项目 1,保持“编辑”按钮不变(不要点击它)。见图2
  3. 然后在选择器中选择列表 2。您应该看到列表中的项目之前有一个额外的空格。此外,所有列表项都不再可滑动。见图3。
  4. 然后在选择器中选择列表 1。它有同样的问题。见图4。

步骤1 第2步 步骤3 步骤4

该问题并非特定于选择器。例如,如果我们用一组按钮替换选择器,就可以重现它。我相信只有当旧列表在 SwiftUI 视图层次结构中被销毁时才会出现此问题。根据我对 SwiftUI 中结构化身份的理解,列表 1 和列表 2 被视为 SwiftUI 视图层次结构中的单独视图。因此尚不清楚它们如何相互影响。我能猜测的唯一原因是,虽然列表 1 和列表 2 被视为单独的虚拟视图,但 SwiftUI 实际上为它们使用相同的物理视图(例如,出于性能目的等)。所以对我来说这似乎是一个 SwiftUI 错误。

沿着这条线思考,我可以通过不破坏列表来解决这个问题:

ZStack {
    createList(Array(1...2), id: .list1)
        .opacity(listID == .list1 ? 1 : 0)
    createList(Array(101...102), id: .list2)
        .opacity(listID == .list2 ? 1 : 0)
}
Run Code Online (Sandbox Code Playgroud)

这在这个特定示例中完美运行,但不幸的是它不可扩展。例如,在我的日历应用程序中,当用户单击日历中的日期时,我想显示该日期的事件列表(我想对不同的日期使用不同的列表。我通过调用来id()设置每个列表都有不同的 ID)。在这种情况下,没有明显/优雅的方法来应用上述解决方案。

所以我想知道是否有人知道如何以更通用的方式解决这个问题?谢谢。

ray*_*ayx 0

我最终通过对不同列表使用单个虚拟视图来解决这个问题。为此,我需要将 List 移到switch语句之外(否则 SwiftUI 的结构化身份机制会将这两个列表视为不同的列表)。

该解决方法在我的测试中可靠地工作(包括在我的实际应用程序中进行测试)。它很干净而且一般。我更喜欢为每个列表分配不同的 id,因为我认为它在架构上更干净且更好,但不幸的是,在 Apple 修复该错误之前它无法使用。我已经提交了关于这个问题的FB9976079。

我将保持我的问题开放,并欢迎任何人留下您的答案或评论。

enum ListID: String, CaseIterable, Hashable, Identifiable {
    case list1 = "List1"
    case list2 = "List2"

    var id: Self {
        self
    }
}

struct ContentView: View {
    @State var listID: ListID = .list1

    var body: some View {
        VStack {
            // 1) Picker
            Picker(selection: $listID) {
                ForEach(ListID.allCases) { id in
                    Text(id.rawValue)
                }
            } label: {
                Text("Select a list")
            }
            .pickerStyle(.segmented)
            // 2) List
            List {
                switch listID {
                case .list1:
                    createSection(Array(1...2), id: .list1)
                case .list2:
                    createSection(Array(101...105), id: .list2)
                }
            }
        }

    }

    // Note: the id param is not used as List id.
    @ViewBuilder func createSection(_ itemValues: [Int], id: ListID) -> some View {
        Section {
            ForEach(itemValues, id:\.self) { value in
                Text("\(value)")
                    .swipeActions(edge: .trailing, allowsFullSwipe: false) {
                        Button("Edit") {
                            // do nothing
                        }
                        .tint(.blue)
                    }
            }
        }
        .id(id)
    }
}
Run Code Online (Sandbox Code Playgroud)