从 SwiftUI 中的工作表更新列表视图中的行
Posted
技术标签:
【中文标题】从 SwiftUI 中的工作表更新列表视图中的行【英文标题】:Update row in list view from sheet in SwiftUI 【发布时间】:2021-12-06 11:54:29 【问题描述】:目标
给定一个列表视图(父)和该列表(子)中的一个行视图,并且鉴于列表视图上有一个工作表模式,我希望允许用户滑动并点击一个“编辑”按钮在列表中的行和显示的工作表中,使用 TextField 更新所选行的内容。
问题和疑问
当工作表打开时,它确实包含由于使用@Binding
而选择的行的内容。但是,一旦关闭工作表,更新此文本字段并不会更新 Row
的内容。
我认为我对@Binding
有一个根本性的误解——我对 SwiftUI 完全陌生。首先,@Binding
是我应该寻找的正确工具吗?我已经看到对Environment
、Observable
和Publisher
的引用——我在这里尝试使用@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")
接下来列表视图显示多个Row
s:
/// 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 现在Row
s 被硬编码在ListView
中。我的下一步是将它们放在一个数组中并进行迭代,最后它们将位于某个数据库或 Firestore 中。但现在我们可以假设Row
s 的可变数组,因为它来自哪里并不重要。
【参考方案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 中的工作表更新列表视图中的行的主要内容,如果未能解决你的问题,请参考以下文章