如何使用 SwiftUI 获得动态视图列表
How to have a dynamic List of Views using SwiftUI
我可以做一个静态列表,比如
List {
View1()
View2()
}
但是我如何从数组中创建一个动态元素列表?
我尝试了以下但出现错误:包含控制流语句的闭包不能与函数构建器一起使用 'ViewBuilder'
let elements: [Any] = [View1.self, View2.self]
List {
ForEach(0..<elements.count) { index in
if let _ = elements[index] as? View1 {
View1()
} else {
View2()
}
}
}
有什么解决办法吗?
我想要完成的是一个包含动态元素集的列表,这些元素不是静态输入的。
if/let
流控制语句不能用在 @ViewBuilder
块中。
那些特殊块内的流控制语句被转换为结构。
例如
if (someBool) {
View1()
} else {
View2()
}
被翻译成ConditionalValue<View1, View2>
。
并非所有流控制语句都可以在这些块中使用,即 switch
,但这可能会在未来发生变化。
有关此内容的更多信息,请参见 function builder evolution proposal。
在你的具体例子中你可以重写代码如下:
struct ContentView : View {
let elements: [Any] = [View1.self, View2.self]
var body: some View {
List {
ForEach(0..<elements.count) { index in
if self.elements[index] is View1 {
View1()
} else {
View2()
}
}
}
}
}
是否可以根据需要return不同View
?
简而言之:有点
正如 swift.org 中的完整描述,IMPOSSIIBLE 有多个 Type returning作为不透明类型
If a function with an opaque return type returns from multiple places, all of the possible return values must have the same type. For a generic function, that return type can use the function’s generic type parameters, but it must still be a single type.
那么 List
如何在静态传递一些不同的视图时做到这一点?
列表不是 returning 不同的类型,它 returns EmptyView
填充了一些内容视图。构建器能够围绕您传递给它的任何类型的视图构建一个包装器,但是当您使用越来越多的视图时,它甚至根本不会编译! (例如尝试传递超过 10 个视图,看看会发生什么)
如您所见,List
内容是某种 ListCoreCellHost
包含的视图子集,证明它只是它所代表内容的容器。
如果我有很多数据(如联系人)并想为此填写一个列表怎么办?
您可以遵循 Identifiable
或使用 identified(by:)
功能,如 所述。
如果任何联系人有不同的看法怎么办?
当你称他们为联系人时,这意味着他们是同一件事!您应该考虑 OOP 使它们相同并利用 inheritance
优势。但与 UIKit
不同的是,SwiftUI
是基于结构的。他们不能相互继承。
那么解决方法是什么?
您必须 将您想要显示的所有类型的视图包装到单个 View
类型中。 EmptyView
的文档不足以利用它(目前)。 但是!!! 幸运的是,你可以使用 UIKit
我怎样才能利用 UIKit
这个
- 在
UIKit
之上实施 View1
和 View2
。
- 用 UIKit 定义一个
ContainerView
。
- 实现
ContainerView
采用参数并表示 View1
或 View2
和适合大小的方式。
- 符合
UIViewRepresentable
并实现它的要求。
- 让你的 SwiftUI
List
显示 ContainerView
的列表
所以现在它是一个可以表示多个视图的单一类型
您可以使用子视图的动态列表,但您需要注意类型和实例化。作为参考,这是一个动态演示 'hamburger',github/swiftui_hamburger。
// Pages View to select current page
/// This could be refactored into the top level
struct Pages: View {
@Binding var currentPage: Int
var pageArray: [AnyView]
var body: AnyView {
return pageArray[currentPage]
}
}
// Top Level View
/// Create two sub-views which, critially, need to be cast to AnyView() structs
/// Pages View then dynamically presents the subviews, based on currentPage state
struct ContentView: View {
@State var currentPage: Int = 0
let page0 = AnyView(
NavigationView {
VStack {
Text("Page Menu").color(.black)
List(["1", "2", "3", "4", "5"].identified(by: \.self)) { row in
Text(row)
}.navigationBarTitle(Text("A Page"), displayMode: .large)
}
}
)
let page1 = AnyView(
NavigationView {
VStack {
Text("Another Page Menu").color(.black)
List(["A", "B", "C", "D", "E"].identified(by: \.self)) { row in
Text(row)
}.navigationBarTitle(Text("A Second Page"), displayMode: .large)
}
}
)
var body: some View {
let pageArray: [AnyView] = [page0, page1]
return Pages(currentPage: self.$currentPage, pageArray: pageArray)
}
}
看起来答案与将我的视图包装在 AnyView
中有关
struct ContentView : View {
var myTypes: [Any] = [View1.self, View2.self]
var body: some View {
List {
ForEach(0..<myTypes.count) { index in
self.buildView(types: self.myTypes, index: index)
}
}
}
func buildView(types: [Any], index: Int) -> AnyView {
switch types[index].self {
case is View1.Type: return AnyView( View1() )
case is View2.Type: return AnyView( View2() )
default: return AnyView(EmptyView())
}
}
}
有了这个,我现在可以从服务器获取 view-data 并组合它们。此外,它们仅在需要时实例化。
我找到了比上面的答案更简单的方法。
创建您的自定义视图。
确保您的视图是 可识别的
(它告诉 SwiftUI 它可以通过查看它们的 id 属性 来区分 ForEach 内部的视图)
例如,假设您只是将图像添加到 HStack,您可以创建自定义 SwiftUI 视图,例如:
struct MyImageView: View, Identifiable {
// Conform to Identifiable:
var id = UUID()
// Name of the image:
var imageName: String
var body: some View {
Image(imageName)
.resizable()
.frame(width: 50, height: 50)
}
}
然后在你的 HStack 中:
// Images:
HStack(spacing: 10) {
ForEach(images, id: \.self) { imageName in
MyImageView(imageName: imageName)
}
Spacer()
}
你可以通过多态来做到这一点:
struct View1: View {
var body: some View {
Text("View1")
}
}
struct View2: View {
var body: some View {
Text("View2")
}
}
class ViewBase: Identifiable {
func showView() -> AnyView {
AnyView(EmptyView())
}
}
class AnyView1: ViewBase {
override func showView() -> AnyView {
AnyView(View1())
}
}
class AnyView2: ViewBase {
override func showView() -> AnyView {
AnyView(View2())
}
}
struct ContentView: View {
let views: [ViewBase] = [
AnyView1(),
AnyView2()
]
var body: some View {
List(self.views) { view in
view.showView()
}
}
}
斯威夫特用户界面 2
您现在可以直接在 @ViewBuilder
块中使用控制流语句,这意味着以下代码完全有效:
struct ContentView: View {
let elements: [Any] = [View1.self, View2.self]
var body: some View {
List {
ForEach(0 ..< elements.count) { index in
if let _ = elements[index] as? View1 {
View1()
} else {
View2()
}
}
}
}
}
斯威夫特用户界面 1
除了已接受的 之外,您还可以使用 @ViewBuilder
并完全避免使用 AnyView
:
@ViewBuilder
func buildView(types: [Any], index: Int) -> some View {
switch types[index].self {
case is View1.Type: View1()
case is View2.Type: View2()
default: EmptyView()
}
}
Swift 5
这似乎对我有用。
struct AMZ1: View {
var body: some View {
Text("Text")
}
}
struct PageView: View {
let elements: [Any] = [AMZ1(), AMZ2(), AMZ3()]
var body: some View {
TabView {
ForEach(0..<elements.count) { index in
if self.elements[index] is AMZ1 {
AMZ1()
} else if self.elements[index] is AMZ2 {
AMZ2()
} else {
AMZ3()
}
}
}
import SwiftUI
struct ContentView: View {
var animationList: [Any] = [
AnimationDemo.self, WithAnimationDemo.self, TransitionDemo.self
]
var body: some View {
NavigationView {
List {
ForEach(0..<animationList.count) { index in
NavigationLink(
destination: animationIndex(types: animationList, index: index),
label: {
listTitle(index: index)
})
}
}
.navigationBarTitle("Animations")
}
}
@ViewBuilder
func listTitle(index: Int) -> some View {
switch index {
case 0:
Text("AnimationDemo").font(.title2).bold()
case 1:
Text("WithAnimationDemo").font(.title2).bold()
case 2:
Text("TransitionDemo").font(.title2).bold()
default:
EmptyView()
}
}
@ViewBuilder
func animationIndex(types: [Any], index: Int) -> some View {
switch types[index].self {
case is AnimationDemo.Type:
AnimationDemo()
case is WithAnimationDemo.Type:
WithAnimationDemo()
case is TransitionDemo.Type:
TransitionDemo()
default:
EmptyView()
}
}
}
enter image description here
我可以做一个静态列表,比如
List {
View1()
View2()
}
但是我如何从数组中创建一个动态元素列表? 我尝试了以下但出现错误:包含控制流语句的闭包不能与函数构建器一起使用 'ViewBuilder'
let elements: [Any] = [View1.self, View2.self]
List {
ForEach(0..<elements.count) { index in
if let _ = elements[index] as? View1 {
View1()
} else {
View2()
}
}
}
有什么解决办法吗? 我想要完成的是一个包含动态元素集的列表,这些元素不是静态输入的。
if/let
流控制语句不能用在 @ViewBuilder
块中。
那些特殊块内的流控制语句被转换为结构。
例如
if (someBool) {
View1()
} else {
View2()
}
被翻译成ConditionalValue<View1, View2>
。
并非所有流控制语句都可以在这些块中使用,即 switch
,但这可能会在未来发生变化。
有关此内容的更多信息,请参见 function builder evolution proposal。
在你的具体例子中你可以重写代码如下:
struct ContentView : View {
let elements: [Any] = [View1.self, View2.self]
var body: some View {
List {
ForEach(0..<elements.count) { index in
if self.elements[index] is View1 {
View1()
} else {
View2()
}
}
}
}
}
是否可以根据需要return不同View
?
简而言之:有点
正如 swift.org 中的完整描述,IMPOSSIIBLE 有多个 Type returning作为不透明类型
If a function with an opaque return type returns from multiple places, all of the possible return values must have the same type. For a generic function, that return type can use the function’s generic type parameters, but it must still be a single type.
那么 List
如何在静态传递一些不同的视图时做到这一点?
列表不是 returning 不同的类型,它 returns EmptyView
填充了一些内容视图。构建器能够围绕您传递给它的任何类型的视图构建一个包装器,但是当您使用越来越多的视图时,它甚至根本不会编译! (例如尝试传递超过 10 个视图,看看会发生什么)
如您所见,List
内容是某种 ListCoreCellHost
包含的视图子集,证明它只是它所代表内容的容器。
如果我有很多数据(如联系人)并想为此填写一个列表怎么办?
您可以遵循 Identifiable
或使用 identified(by:)
功能,如
如果任何联系人有不同的看法怎么办?
当你称他们为联系人时,这意味着他们是同一件事!您应该考虑 OOP 使它们相同并利用 inheritance
优势。但与 UIKit
不同的是,SwiftUI
是基于结构的。他们不能相互继承。
那么解决方法是什么?
您必须 将您想要显示的所有类型的视图包装到单个 View
类型中。 EmptyView
的文档不足以利用它(目前)。 但是!!! 幸运的是,你可以使用 UIKit
我怎样才能利用 UIKit
这个
- 在
UIKit
之上实施View1
和View2
。 - 用 UIKit 定义一个
ContainerView
。 - 实现
ContainerView
采用参数并表示View1
或View2
和适合大小的方式。 - 符合
UIViewRepresentable
并实现它的要求。 - 让你的 SwiftUI
List
显示ContainerView
的列表 所以现在它是一个可以表示多个视图的单一类型
您可以使用子视图的动态列表,但您需要注意类型和实例化。作为参考,这是一个动态演示 'hamburger',github/swiftui_hamburger。
// Pages View to select current page
/// This could be refactored into the top level
struct Pages: View {
@Binding var currentPage: Int
var pageArray: [AnyView]
var body: AnyView {
return pageArray[currentPage]
}
}
// Top Level View
/// Create two sub-views which, critially, need to be cast to AnyView() structs
/// Pages View then dynamically presents the subviews, based on currentPage state
struct ContentView: View {
@State var currentPage: Int = 0
let page0 = AnyView(
NavigationView {
VStack {
Text("Page Menu").color(.black)
List(["1", "2", "3", "4", "5"].identified(by: \.self)) { row in
Text(row)
}.navigationBarTitle(Text("A Page"), displayMode: .large)
}
}
)
let page1 = AnyView(
NavigationView {
VStack {
Text("Another Page Menu").color(.black)
List(["A", "B", "C", "D", "E"].identified(by: \.self)) { row in
Text(row)
}.navigationBarTitle(Text("A Second Page"), displayMode: .large)
}
}
)
var body: some View {
let pageArray: [AnyView] = [page0, page1]
return Pages(currentPage: self.$currentPage, pageArray: pageArray)
}
}
看起来答案与将我的视图包装在 AnyView
struct ContentView : View {
var myTypes: [Any] = [View1.self, View2.self]
var body: some View {
List {
ForEach(0..<myTypes.count) { index in
self.buildView(types: self.myTypes, index: index)
}
}
}
func buildView(types: [Any], index: Int) -> AnyView {
switch types[index].self {
case is View1.Type: return AnyView( View1() )
case is View2.Type: return AnyView( View2() )
default: return AnyView(EmptyView())
}
}
}
有了这个,我现在可以从服务器获取 view-data 并组合它们。此外,它们仅在需要时实例化。
我找到了比上面的答案更简单的方法。
创建您的自定义视图。
确保您的视图是 可识别的
(它告诉 SwiftUI 它可以通过查看它们的 id 属性 来区分 ForEach 内部的视图)
例如,假设您只是将图像添加到 HStack,您可以创建自定义 SwiftUI 视图,例如:
struct MyImageView: View, Identifiable {
// Conform to Identifiable:
var id = UUID()
// Name of the image:
var imageName: String
var body: some View {
Image(imageName)
.resizable()
.frame(width: 50, height: 50)
}
}
然后在你的 HStack 中:
// Images:
HStack(spacing: 10) {
ForEach(images, id: \.self) { imageName in
MyImageView(imageName: imageName)
}
Spacer()
}
你可以通过多态来做到这一点:
struct View1: View {
var body: some View {
Text("View1")
}
}
struct View2: View {
var body: some View {
Text("View2")
}
}
class ViewBase: Identifiable {
func showView() -> AnyView {
AnyView(EmptyView())
}
}
class AnyView1: ViewBase {
override func showView() -> AnyView {
AnyView(View1())
}
}
class AnyView2: ViewBase {
override func showView() -> AnyView {
AnyView(View2())
}
}
struct ContentView: View {
let views: [ViewBase] = [
AnyView1(),
AnyView2()
]
var body: some View {
List(self.views) { view in
view.showView()
}
}
}
斯威夫特用户界面 2
您现在可以直接在 @ViewBuilder
块中使用控制流语句,这意味着以下代码完全有效:
struct ContentView: View {
let elements: [Any] = [View1.self, View2.self]
var body: some View {
List {
ForEach(0 ..< elements.count) { index in
if let _ = elements[index] as? View1 {
View1()
} else {
View2()
}
}
}
}
}
斯威夫特用户界面 1
除了已接受的 @ViewBuilder
并完全避免使用 AnyView
:
@ViewBuilder
func buildView(types: [Any], index: Int) -> some View {
switch types[index].self {
case is View1.Type: View1()
case is View2.Type: View2()
default: EmptyView()
}
}
Swift 5
这似乎对我有用。
struct AMZ1: View {
var body: some View {
Text("Text")
}
}
struct PageView: View {
let elements: [Any] = [AMZ1(), AMZ2(), AMZ3()]
var body: some View {
TabView {
ForEach(0..<elements.count) { index in
if self.elements[index] is AMZ1 {
AMZ1()
} else if self.elements[index] is AMZ2 {
AMZ2()
} else {
AMZ3()
}
}
}
import SwiftUI
struct ContentView: View {
var animationList: [Any] = [
AnimationDemo.self, WithAnimationDemo.self, TransitionDemo.self
]
var body: some View {
NavigationView {
List {
ForEach(0..<animationList.count) { index in
NavigationLink(
destination: animationIndex(types: animationList, index: index),
label: {
listTitle(index: index)
})
}
}
.navigationBarTitle("Animations")
}
}
@ViewBuilder
func listTitle(index: Int) -> some View {
switch index {
case 0:
Text("AnimationDemo").font(.title2).bold()
case 1:
Text("WithAnimationDemo").font(.title2).bold()
case 2:
Text("TransitionDemo").font(.title2).bold()
default:
EmptyView()
}
}
@ViewBuilder
func animationIndex(types: [Any], index: Int) -> some View {
switch types[index].self {
case is AnimationDemo.Type:
AnimationDemo()
case is WithAnimationDemo.Type:
WithAnimationDemo()
case is TransitionDemo.Type:
TransitionDemo()
default:
EmptyView()
}
}
}
enter image description here