在 SwiftUI 中将数据传递到 ViewBuilder 闭包的正确方法是什么?
What is the correct method for passing data into a ViewBuilder closure in SwiftUI?
我在 SwiftUI 中使用泛型,运行 在尝试利用 ViewBuilder 闭包将数据传递到通用视图时遇到了数据持久性问题。我的目标是拥有一个 shell 视图来管理从 API 接收数据并将其传递给通用视图,如 ViewBuilder 块中所定义的那样。所有数据似乎都已成功传递给初始化,包括我的通用 BasicListView
,但是当 body
被调用时,列表数据的 none 被保留。
我认为通过代码来解释问题会更容易。在这里为长代码转储道歉:
import SwiftUI
import Combine
// This is the blank "shell" View that manages passing the data into the viewBuilder through the @ViewBuilder block
struct BlankView<ListItem, Content:View>: View where ListItem: Listable {
let api = GlobalAPI.shared
@State var list: [ListItem] = []
@State var viewBuilder: ([ListItem]) -> Content // Passing in generic [ListItem] here
init(@ViewBuilder builder: @escaping ([ListItem]) -> Content) {
self._viewBuilder = State<([ListItem]) -> Content>(initialValue: builder)
}
var body: some View {
viewBuilder(list) // List contained in Blank View passed into viewBuilder Block here
.multilineTextAlignment(.center)
.onReceive(GlobalAPI.shared.listDidChange) { item in
if let newItem = item as? ListItem {
self.list.append(newItem) // Handle API updates here
}
}
}
}
// And Here is the implementation of the Blank View
struct TestView: View {
public var body: some View {
BlankView<MockListItem, VStack>() { items in // A list of items will get passed into the block
VStack {
Text("Add a row") // Button to add row via API singleton
.onTapGesture {
GlobalAPI.shared.addListItem()
}
BasicListView(items: items) { // List view init'd with items
Text("Hold on to your butts") // Destination
}
}
}
}
}
// Supporting code
// The generic list view/cell
struct BasicListView<Content: View, ListItem:Listable>: View {
@State var items: [ListItem]
var destination: () -> Content
init(items: [ListItem], @ViewBuilder builder: @escaping () -> Content) {
self._items = State<[ListItem]>(initialValue: items) // Items successfully init'd here
self.destination = builder
}
var body: some View {
List(items) { item in // Items that were passed into init no longer present here, this runs on a blank [ListItem] array
BasicListCell(item: item, destination: self.destination)
}
}
}
struct BasicListCell<Content: View, ListItem:Listable>: View {
@State var item: ListItem
var destination: () -> Content
var body: some View {
NavigationLink(destination: destination()) {
HStack {
item.photo
.resizable()
.frame(width: 50.0, height: 50.0)
.font(.largeTitle)
.cornerRadius(25.0)
VStack (alignment: .leading) {
Text(item.title)
.font(.headline)
Text(item.description)
.font(.subheadline)
.foregroundColor(.secondary)
}
}
}
}
}
// The protocol and mock data struct
protocol Listable: Identifiable {
var id: UUID { get set }
var title: String { get set }
var description: String { get set }
var photo: Image { get set }
}
public struct MockListItem: Listable {
public var photo: Image = Image(systemName:"photo")
public var id = UUID()
public var title: String = "Title"
public var description: String = "This is the description"
static let all = [MockListItem(), MockListItem(), MockListItem(), MockListItem()]
}
// A global API singleton for testing data updates
class GlobalAPI {
static let shared = GlobalAPI()
var listDidChange = PassthroughSubject<MockListItem, Never>()
var newListItem:MockListItem? = nil {
didSet {
if let item = newListItem {
listDidChange.send(item)
}
}
}
func addListItem() {
newListItem = MockListItem()
}
}
这是 ViewBuilder 块的正确实现,还是不鼓励尝试通过 View 构建器块传递数据?
注意:什么有效
如果我直接传入静态 Mock 数据,视图将正确绘制自己,如下所示:
struct TestView: View {
public var body: some View {
BlankView<MockListItem, VStack>() { items in // A list of items will get passed into the block
VStack {
Text("Add a row") // Button to add row via API singleton
.onTapGesture {
GlobalAPI.shared.addListItem()
}
BasicListView(items: MockListItem.all) { // List view init'd with items
Text("Hold on to your butts") // Destination
}
}
}
}
}
有什么想法吗?感谢大家的帮助和反馈。
好吧,我想我找到了解决办法。
问题似乎出在 BasicListView
中的项目被包裹在 @State
而不是 @Binding
中,并且 ViewBuilder 块的类型为 ([ListItem]) -> Content
(Binding<[ListItem]>) -> Content
个。最初的设置用于从块外部提取的静态数据 (MockListItem.all
) 进行初始化,但是当使用传递到块中的数据时,在 init 和被调用的主体之间的某处是 discarded/reset。相反,我将 BasicListView
中的 items
更改为 @Binding,现在通过传入 BlankView
中的 @State var list
的绑定来初始化。这是更新后的代码:
// This is the blank "shell" View that manages passing the data into the viewBuilder through the @ViewBuilder block
struct BlankView<ListItem, Content:View>: View where ListItem: Listable {
let api = GlobalAPI.shared
@State var list: [ListItem] = []
var viewBuilder: (Binding<[ListItem]>) -> Content // Now passing Binding into the block instead of an array
init(contentType: ContentType, @ViewBuilder builder: @escaping (Binding<[ListItem]>) -> Content) {
self.viewBuilder = builder
}
var body: some View {
viewBuilder($list) // Binding passed into ViewBuilder block
.multilineTextAlignment(.center)
.onReceive(GlobalAPI.shared.listDidChange) { item in
if let newItem = item as? ListItem {
self.list.append(newItem) // Handle API updates here
}
}
}
}
// Supporting code
// The generic list view/cell
struct BasicListView<Content: View, ListItem:Listable>: View {
@Binding var items: [ListItem]
var destination: () -> Content
init(items: Binding<[ListItem]>, @ViewBuilder builder: @escaping () -> Content) {
self._items = items
self.destination = builder
}
var body: some View {
List(items) { item in // Items passed into init now persist and correctly get rendered here, including when API updates the list.
BasicListCell(item: item, destination: self.destination)
}
}
}
希望这对外面的人有所帮助。干杯!
这里是固定视图。您在外部提供模型,但状态用于内部更改,并且一旦创建它就会持续存在于同一视图中。所以在这种情况下状态是错误的——视图重建是由外部注入的数据管理的。
测试 Xcode 11.4 / iOS 13.4
struct BasicListView<Content: View, ListItem:Listable>: View {
var items: [ListItem]
var destination: () -> Content
init(items: [ListItem], @ViewBuilder builder: @escaping () -> Content) {
self.items = items // Items successfully init'd here
self.destination = builder
}
var body: some View {
List(items) { item in // Items that were passed into init no longer present here, this runs on a blank [ListItem] array
BasicListCell(item: item, destination: self.destination)
}
}
}
我在 SwiftUI 中使用泛型,运行 在尝试利用 ViewBuilder 闭包将数据传递到通用视图时遇到了数据持久性问题。我的目标是拥有一个 shell 视图来管理从 API 接收数据并将其传递给通用视图,如 ViewBuilder 块中所定义的那样。所有数据似乎都已成功传递给初始化,包括我的通用 BasicListView
,但是当 body
被调用时,列表数据的 none 被保留。
我认为通过代码来解释问题会更容易。在这里为长代码转储道歉:
import SwiftUI
import Combine
// This is the blank "shell" View that manages passing the data into the viewBuilder through the @ViewBuilder block
struct BlankView<ListItem, Content:View>: View where ListItem: Listable {
let api = GlobalAPI.shared
@State var list: [ListItem] = []
@State var viewBuilder: ([ListItem]) -> Content // Passing in generic [ListItem] here
init(@ViewBuilder builder: @escaping ([ListItem]) -> Content) {
self._viewBuilder = State<([ListItem]) -> Content>(initialValue: builder)
}
var body: some View {
viewBuilder(list) // List contained in Blank View passed into viewBuilder Block here
.multilineTextAlignment(.center)
.onReceive(GlobalAPI.shared.listDidChange) { item in
if let newItem = item as? ListItem {
self.list.append(newItem) // Handle API updates here
}
}
}
}
// And Here is the implementation of the Blank View
struct TestView: View {
public var body: some View {
BlankView<MockListItem, VStack>() { items in // A list of items will get passed into the block
VStack {
Text("Add a row") // Button to add row via API singleton
.onTapGesture {
GlobalAPI.shared.addListItem()
}
BasicListView(items: items) { // List view init'd with items
Text("Hold on to your butts") // Destination
}
}
}
}
}
// Supporting code
// The generic list view/cell
struct BasicListView<Content: View, ListItem:Listable>: View {
@State var items: [ListItem]
var destination: () -> Content
init(items: [ListItem], @ViewBuilder builder: @escaping () -> Content) {
self._items = State<[ListItem]>(initialValue: items) // Items successfully init'd here
self.destination = builder
}
var body: some View {
List(items) { item in // Items that were passed into init no longer present here, this runs on a blank [ListItem] array
BasicListCell(item: item, destination: self.destination)
}
}
}
struct BasicListCell<Content: View, ListItem:Listable>: View {
@State var item: ListItem
var destination: () -> Content
var body: some View {
NavigationLink(destination: destination()) {
HStack {
item.photo
.resizable()
.frame(width: 50.0, height: 50.0)
.font(.largeTitle)
.cornerRadius(25.0)
VStack (alignment: .leading) {
Text(item.title)
.font(.headline)
Text(item.description)
.font(.subheadline)
.foregroundColor(.secondary)
}
}
}
}
}
// The protocol and mock data struct
protocol Listable: Identifiable {
var id: UUID { get set }
var title: String { get set }
var description: String { get set }
var photo: Image { get set }
}
public struct MockListItem: Listable {
public var photo: Image = Image(systemName:"photo")
public var id = UUID()
public var title: String = "Title"
public var description: String = "This is the description"
static let all = [MockListItem(), MockListItem(), MockListItem(), MockListItem()]
}
// A global API singleton for testing data updates
class GlobalAPI {
static let shared = GlobalAPI()
var listDidChange = PassthroughSubject<MockListItem, Never>()
var newListItem:MockListItem? = nil {
didSet {
if let item = newListItem {
listDidChange.send(item)
}
}
}
func addListItem() {
newListItem = MockListItem()
}
}
这是 ViewBuilder 块的正确实现,还是不鼓励尝试通过 View 构建器块传递数据?
注意:什么有效
如果我直接传入静态 Mock 数据,视图将正确绘制自己,如下所示:
struct TestView: View {
public var body: some View {
BlankView<MockListItem, VStack>() { items in // A list of items will get passed into the block
VStack {
Text("Add a row") // Button to add row via API singleton
.onTapGesture {
GlobalAPI.shared.addListItem()
}
BasicListView(items: MockListItem.all) { // List view init'd with items
Text("Hold on to your butts") // Destination
}
}
}
}
}
有什么想法吗?感谢大家的帮助和反馈。
好吧,我想我找到了解决办法。
问题似乎出在 BasicListView
中的项目被包裹在 @State
而不是 @Binding
中,并且 ViewBuilder 块的类型为 ([ListItem]) -> Content
(Binding<[ListItem]>) -> Content
个。最初的设置用于从块外部提取的静态数据 (MockListItem.all
) 进行初始化,但是当使用传递到块中的数据时,在 init 和被调用的主体之间的某处是 discarded/reset。相反,我将 BasicListView
中的 items
更改为 @Binding,现在通过传入 BlankView
中的 @State var list
的绑定来初始化。这是更新后的代码:
// This is the blank "shell" View that manages passing the data into the viewBuilder through the @ViewBuilder block
struct BlankView<ListItem, Content:View>: View where ListItem: Listable {
let api = GlobalAPI.shared
@State var list: [ListItem] = []
var viewBuilder: (Binding<[ListItem]>) -> Content // Now passing Binding into the block instead of an array
init(contentType: ContentType, @ViewBuilder builder: @escaping (Binding<[ListItem]>) -> Content) {
self.viewBuilder = builder
}
var body: some View {
viewBuilder($list) // Binding passed into ViewBuilder block
.multilineTextAlignment(.center)
.onReceive(GlobalAPI.shared.listDidChange) { item in
if let newItem = item as? ListItem {
self.list.append(newItem) // Handle API updates here
}
}
}
}
// Supporting code
// The generic list view/cell
struct BasicListView<Content: View, ListItem:Listable>: View {
@Binding var items: [ListItem]
var destination: () -> Content
init(items: Binding<[ListItem]>, @ViewBuilder builder: @escaping () -> Content) {
self._items = items
self.destination = builder
}
var body: some View {
List(items) { item in // Items passed into init now persist and correctly get rendered here, including when API updates the list.
BasicListCell(item: item, destination: self.destination)
}
}
}
希望这对外面的人有所帮助。干杯!
这里是固定视图。您在外部提供模型,但状态用于内部更改,并且一旦创建它就会持续存在于同一视图中。所以在这种情况下状态是错误的——视图重建是由外部注入的数据管理的。
测试 Xcode 11.4 / iOS 13.4
struct BasicListView<Content: View, ListItem:Listable>: View {
var items: [ListItem]
var destination: () -> Content
init(items: [ListItem], @ViewBuilder builder: @escaping () -> Content) {
self.items = items // Items successfully init'd here
self.destination = builder
}
var body: some View {
List(items) { item in // Items that were passed into init no longer present here, this runs on a blank [ListItem] array
BasicListCell(item: item, destination: self.destination)
}
}
}