从 SwiftUI 中的工作表更新列表视图中的行

Posted

技术标签:

【中文标题】从 SwiftUI 中的工作表更新列表视图中的行【英文标题】:Update row in list view from sheet in SwiftUI 【发布时间】:2021-12-06 11:54:29 【问题描述】:

目标

给定一个列表视图(父)和该列表(子)中的一个行视图,并且鉴于列表视图上有一个工作表模式,我希望允许用户滑动并点击一个“编辑”按钮在列表中的行和显示的工作表中,使用 TextField 更新所选行的内容。

问题和疑问

当工作表打开时,它确实包含由于使用@Binding 而选择的行的内容。但是,一旦关闭工作表,更新此文本字段并不会更新 Row 的内容。

我认为我对@Binding 有一个根本性的误解——我对 SwiftUI 完全陌生。首先,@Binding 是我应该寻找的正确工具吗?我已经看到对EnvironmentObservablePublisher 的引用——我在这里尝试使用@Binding 是不是“做错了”?还是只是我犯了一些愚蠢的小错误,只是看不到它?

其次,我觉得很奇怪,我必须在一行本身内设置选定的行,只是为了绑定它,以便它在父列表中可用。这只是感觉有点不对劲。有什么更好的选择?

正在播放的视频:https://streamable.com/805fsp

代码

请注意,所有这些都可以放在一个单独的 SwiftUI 文件中,以便您自己查看它是否失败。为了便于理解 SoC,我将它们与标题分开,并将我的问题提炼为我能想到的最简单的示例

首先,包含名称和描述的小型简单结构。

/// Simple entity containing data to be displayed in a row view.
struct Row 
    var name: String
    var description: String

接下来,将在页面上表示 Row 结构的视图:

/// A single row to be displayed within a List with a swipe actoun.
struct RowView: View 
    @Binding var isEditing: Bool
    @Binding var rowBeingEdited: Row
    
    // Every RowView is provided a Row entity to draw it's data from.
    @State var row: Row

    var body: some View 
        HStack 
            Text(self.row.name)
            Text(self.row.description)
        
        // Edit Button.
        .swipeActions(edge: .leading, allowsFullSwipe: false) 
            Button(action: 
                self.rowBeingEdited = self.row
                self.isEditing = true
            ) 
                Label("Edit", systemImage: "rectangle.and.pencil.and.ellipsis")
            
        
    

接下来列表视图显示多个Rows:

/// The list view containing multiple RowViews.
struct ListView: View 
    // ListView will be told when a row is selected to be edited.
    @State var isEditing: Bool

    // ListView needs to know which row is being edited to pass to the modal sheet
    @State var rowBeingEdited: Row
    
    var body: some View 
        List 
            RowView(
                isEditing: $isEditing,
                rowBeingEdited: $rowBeingEdited,
                row: Row(name: "Name1", description: "Description1")
            )
            RowView(
                isEditing: $isEditing,
                rowBeingEdited: $rowBeingEdited,
                row: Row(name: "Name2", description: "Description2")
            )
        
        .sheet(isPresented: $isEditing) 
            EditSheet(rowBeingEdited: $rowBeingEdited)
        
    

最后选择一行后要显示的工作表模式:

/// The sheet view modal used to edit the row.
struct EditSheet: View 
    @Binding var rowBeingEdited: Row
    
    // My expectation: Changing value of text field updates row here, so in list view, so in row view. Why isn't it doing this?
    var body: some View 
        VStack 
            TextField("Name", text: $rowBeingEdited.name)
            TextField("Description", text: $rowBeingEdited.description)
        
    

一些用于在 Xcode 中显示预览的代码。顺便说一句,也许有更好的方法?

struct RowTest_Previews: PreviewProvider 
    static var previews: some View 
        let testPreview = RowTestRow_Preview(rowBeingEdited: Row(name: "N", description: "D"))

        ListView(
            isEditing: false,
            rowBeingEdited: testPreview.rowBeingEdited
        )
    


struct RowTestRow_Preview: View 
    @State var rowBeingEdited: Row

    var body: some View 
        Text("")
    


【问题讨论】:

你的数据模型是什么?例如在这个问题中,是像你一样固定几行,还是我们在谈论[Row] @Yrb 现在Rows 被硬编码在ListView 中。我的下一步是将它们放在一个数组中并进行迭代,最后它们将位于某个数据库或 Firestore 中。但现在我们可以假设Rows 的可变数组,因为它来自哪里并不重要。 【参考方案1】:

在这种情况下,从数据数组开始实际上在概念上更简单。另外,我使您的 Row 结构可识别,因为我们需要跟踪它。在 SwiftUI 中处理数据数组时,您应该本能地遵守Identifiable。事情变得无比简单。

您认为您需要为每一行提供个性化的解决方案,但没有将数据放在数组中,这导致您将该代码放入您的 RowView,这实际上使事情复杂化。在ForEach 中处理要容易得多,因为您需要识别特定行的一切都在那里。

其中最复杂的部分没有 @State 变量来跟踪呈现工作表。一旦您将工作表代码从行中拉出,这将导致所有工作表尝试触发。我用.sheet(isPresented:) 中的计算Binding 替换了对它的需求,它将@State var rowID 与当前行ID 进行比较。如果它们匹配,那么这就是我们想要的行并且工作表触发。否则,什么都不会发生。 rowID 只需在.swipeAction() 按钮中设置。

struct ListView: View 
    @State var rows: [Row] = [Row(name: "Name1", description: "Description1"),
                           Row(name: "Name2", description: "Description2"),
                           Row(name: "Name3", description: "Description3"),
                           Row(name: "Name4", description: "Description4"),
                           Row(name: "Name5", description: "Description5"),
                           Row(name: "Name6", description: "Description6")]
    @State var rowID: UUID?
    
    var body: some View 
        List 
            ForEach($rows)  $row in
                RowView(row: row)
                    .swipeActions(edge: .leading, allowsFullSwipe: false) 
                        Button(action: 
                            rowID = row.id
                        ) 
                            Label("Edit", systemImage: "rectangle.and.pencil.and.ellipsis")
                        
                    
                    .sheet(isPresented: Binding<Bool>(
                        get:  row.id == rowID ,
                        set:  _ in
                            rowID = nil
                        )
                    ) 
                        EditSheet(rowBeingEdited: $row)
                    
            
        
    

RowView 现在没有逻辑,所以我可以拉出我们所有的 @State@Binding 并将其变成一个简单的显示视图:

/// A single row to be displayed within a List with a swipe actoun.
struct RowView: View 
    // Every RowView is provided a Row entity to draw it's data from.
    let row: Row

    var body: some View 
        HStack 
            Text(row.name)
            Text(row.description)
        
    


/// Simple entity containing data to be displayed in a row view.
struct Row: Identifiable 
    let id = UUID()
    var name: String
    var description: String

最后,您要求提供一些预览提示。我通过将数据直接置于ListView 状态来简化数据的显示,但有时您需要@State 来提供绑定以预览视图,因为它通常会从其他地方获取数据(例如你原来的RowView。实际上很难从预览提供程序中注入它,它在实时预览中工作。在这种情况下,我将创建一个我称之为Intermediary View" which is a simple View`的结构,我可以在其中创建任何状态需要运行预览视图。该结构然后调用我的预览视图并提供数据。然后我从预览提供程序调用“中间视图”。它很简单并且效果很好。

编辑:

“中间视图”示例

假设您的ListView 调用@Binding 而不是@State

struct ListViewFromSheet: View 
    @Binding var rows: [Row]
    ...

然后你会这样写你的“中间视图”:

struct IntermediaryListView: View 

    @State var rows: [Row] = [Row(name: "Name1", description: "Description1"),
                           Row(name: "Name2", description: "Description2"),
                           Row(name: "Name3", description: "Description3"),
                           Row(name: "Name4", description: "Description4"),
                           Row(name: "Name5", description: "Description5"),
                           Row(name: "Name6", description: "Description6")]

    var body: some View 
        ListView(rows: $rows)
    

然后您的预览提供者只需调用中间视图:

struct ListViewFromSheet_Previews: PreviewProvider 
    static var previews: some View 
        IntermediaryListView()
    

这个例子有点做作,因为如果您需要所有这些视图的数据,您可能会在所有视图都可以获取数据的地方提供它,但是在您可能有 @State var 的地方它同样有效在视图中充当状态标志的层次结构是@Binding。假设您的视图有一个更改标志的按钮,您需要在视图中测试更改该标志的结果。在预览提供程序中进行设置时,您需要有一个@State static var 来设置它。虽然这不是很糟糕,但事情会变得更加复杂,并且更容易将一些代码嵌入到常规 View 结构中,并且这些代码通常可以直接从调用 View 中复制。

【讨论】:

非常感谢!我必须尽快试一试。第一个问题:如果您说 500 行,新的 get/set 事物是否会导致性能问题,因为它会在所有 500 行中循环查找 `row.id == rowID` 的位置还是可以?其次,你有没有机会举一个你的中间观点的例子,因为我很确定我会在任何地方使用它!非常感谢! :) 它应该有更好的性能特征。当您的数据是一个数组时,您仍然需要具有相同的基本结构,但是使用@State@Binding,您的 RowView 结构要贵得多。此外,作为 SwiftUI 优化的一部分,只会在必要时创建行。换句话说,您不会搜索 500 行。此外,中间视图没有什么特别之处,但我发布了一个示例。

以上是关于从 SwiftUI 中的工作表更新列表视图中的行的主要内容,如果未能解决你的问题,请参考以下文章

为啥单击 SwiftUI 中的列表项后列表视图项变为灰色?

SwiftUI:从工作表导航到新视图

如果我更新一个表的行,它也是视图中的一行,视图的行也会更新吗?

基于外部单例类中的属性的 SwiftUI 显示表

更新和删除列表视图中的行

使用jQuery更新行表(数据网格)时出现问题