替代 SwiftUI ViewBuilder 块中的 switch 语句?
Alternative to switch statement in SwiftUI ViewBuilder block?
⚠️ 2020 年 6 月 23 日编辑:从 Xcode12 起,ViewBuilder 将支持 switch 和 if let 语句!
我一直在尝试使用 SwiftUI 复制我的一个应用程序。它有一个 RootViewController,它根据枚举值显示不同的子视图控制器。与在 SwiftUI 中一样,我们使用视图而不是视图控制器,我的代码如下所示:
struct RootView : View {
@State var containedView: ContainedView = .home
var body: some View {
// custom header goes here
switch containedView {
case .home: HomeView()
case .categories: CategoriesView()
...
}
}
}
不幸的是,我收到警告:
Closure containing control flow statement cannot be used with function builder ViewBuilder
.
那么,是否有任何替代方法可以让我复制这种行为?
您必须将代码包装在视图中,例如 VStack
或 Group
:
var body: some View {
Group {
switch containedView {
case .home: HomeView()
case .categories: CategoriesView()
...
}
}
}
或者,添加 return 值应该有效:
var body: some View {
switch containedView {
case .home: return HomeView()
case .categories: return CategoriesView()
...
}
}
但是,best-practice 解决此问题的方法是创建一个 return 视图的方法:
func nextView(for containedView: YourViewEnum) -> some AnyView {
switch containedView {
case .home: return HomeView()
case .categories: return CategoriesView()
...
}
}
var body: some View {
nextView(for: containedView)
}
⚠️ 2020 年 6 月 23 日编辑:从 Xcode12 开始,ViewBuilder 将支持 switch 和 if let 语句!
谢谢你们的回答,伙计们。我在 Apple 的开发论坛 上找到了解决方案。
Kiel Gillard 回答了这个问题。解决方案是按照 Lu_、Linus 和 Mo 的建议将开关提取到函数中,但我们必须将视图包装在 AnyView
中才能正常工作——像这样:
struct RootView: View {
@State var containedViewType: ContainedViewType = .home
var body: some View {
VStack {
// custom header goes here
containedView()
}
}
func containedView() -> AnyView {
switch containedViewType {
case .home: return AnyView(HomeView())
case .categories: return AnyView(CategoriesView())
...
}
}
如果您指定 ViewBuilder
的 return 类型,您似乎不需要将 switch 语句提取到单独的函数中。例如:
Group { () -> Text in
switch status {
case .on:
return Text("On")
case .off:
return Text("Off")
}
}
Note: You can also return arbitrary view types if you wrap them in AnyView
and specify that as the return type.
更新:SwiftUI 2 现在支持函数构建器中的 switch 语句,https://github.com/apple/swift/pull/30174
添加到 Nikolai 的答案中,该答案编译了开关但不使用转换,这是他的示例的一个版本,它支持转换。
struct RootView: View {
@State var containedViewType: ContainedViewType = .home
var body: some View {
VStack {
// custom header goes here
containedView()
}
}
func containedView() -> some View {
switch containedViewType {
case .home: return AnyView(HomeView()).id("HomeView")
case .categories: return AnyView(CategoriesView()).id("CategoriesView")
...
}
}
请注意已添加到每个 AnyView 的 id(...)
。这允许 SwiftUI 识别其视图层次结构中的视图,从而允许它正确应用过渡动画。
不使用 AnyView()。我将使用一堆 if 语句并在我的枚举中实现协议 Equatable 和 CustomStringConvertible 以检索我的关联值:
var body: some View {
ZStack {
Color("background1")
.edgesIgnoringSafeArea(.all)
.onAppear { self.viewModel.send(event: .onAppear) }
// You can use viewModel.state == .loading as well if your don't have
// associated values
if viewModel.state.description == "loading" {
LoadingContentView()
} else if viewModel.state.description == "idle" {
IdleContentView()
} else if viewModel.state.description == "loaded" {
LoadedContentView(list: viewModel.state.value as! [AnimeItem])
} else if viewModel.state.description == "error" {
ErrorContentView(error: viewModel.state.value as! Error)
}
}
}
我将使用结构分隔我的视图:
struct ErrorContentView: View {
var error: Error
var body: some View {
VStack {
Image("error")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 100)
Text(error.localizedDescription)
}
}
}
您可以使用包装视图
struct MakeView: View {
let make: () -> AnyView
var body: some View {
make()
}
}
struct UseMakeView: View {
let animal: Animal = .cat
var body: some View {
MakeView {
switch self.animal {
case .cat:
return Text("cat").erase()
case .dog:
return Text("dog").erase()
case .mouse:
return Text("mouse").erase()
}
}
}
}
您可以将枚举与 @ViewBuilder
一起使用,如下所示...
清除枚举
enum Destination: CaseIterable, Identifiable {
case restaurants
case profile
var id: String { return title }
var title: String {
switch self {
case .restaurants: return "Restaurants"
case .profile: return "Profile"
}
}
}
现在在查看文件中
struct ContentView: View {
@State private var selectedDestination: Destination? = .restaurants
var body: some View {
NavigationView {
view(for: selectedDestination)
}
}
@ViewBuilder
func view(for destination: Destination?) -> some View {
switch destination {
case .some(.restaurants):
CategoriesView()
case .some(.profile):
ProfileView()
default:
EmptyView()
}
}
}
如果您想使用与 NavigationLink 相同的大小写...您可以按如下方式使用它
struct ContentView: View {
@State private var selectedDestination: Destination? = .restaurants
var body: some View {
NavigationView {
List(Destination.allCases,
selection: $selectedDestination) { item in
NavigationLink(destination: view(for: selectedDestination),
tag: item,
selection: $selectedDestination) {
Text(item.title).tag(item)
}
}
}
}
@ViewBuilder
func view(for destination: Destination?) -> some View {
switch destination {
case .some(.restaurants):
CategoriesView()
case .some(.profile):
ProfileView()
default:
EmptyView()
}
}
}
在 switch
中提供 default
语句为我解决了这个问题:
struct RootView : View {
@State var containedView: ContainedView = .home
var body: some View {
// custom header goes here
switch containedView {
case .home: HomeView()
case .categories: CategoriesView()
...
default: EmptyView()
}
}
}
⚠️ 2020 年 6 月 23 日编辑:从 Xcode12 起,ViewBuilder 将支持 switch 和 if let 语句!
我一直在尝试使用 SwiftUI 复制我的一个应用程序。它有一个 RootViewController,它根据枚举值显示不同的子视图控制器。与在 SwiftUI 中一样,我们使用视图而不是视图控制器,我的代码如下所示:
struct RootView : View {
@State var containedView: ContainedView = .home
var body: some View {
// custom header goes here
switch containedView {
case .home: HomeView()
case .categories: CategoriesView()
...
}
}
}
不幸的是,我收到警告:
Closure containing control flow statement cannot be used with function builder
ViewBuilder
.
那么,是否有任何替代方法可以让我复制这种行为?
您必须将代码包装在视图中,例如 VStack
或 Group
:
var body: some View {
Group {
switch containedView {
case .home: HomeView()
case .categories: CategoriesView()
...
}
}
}
或者,添加 return 值应该有效:
var body: some View {
switch containedView {
case .home: return HomeView()
case .categories: return CategoriesView()
...
}
}
但是,best-practice 解决此问题的方法是创建一个 return 视图的方法:
func nextView(for containedView: YourViewEnum) -> some AnyView {
switch containedView {
case .home: return HomeView()
case .categories: return CategoriesView()
...
}
}
var body: some View {
nextView(for: containedView)
}
⚠️ 2020 年 6 月 23 日编辑:从 Xcode12 开始,ViewBuilder 将支持 switch 和 if let 语句!
谢谢你们的回答,伙计们。我在 Apple 的开发论坛 上找到了解决方案。
Kiel Gillard 回答了这个问题。解决方案是按照 Lu_、Linus 和 Mo 的建议将开关提取到函数中,但我们必须将视图包装在 AnyView
中才能正常工作——像这样:
struct RootView: View {
@State var containedViewType: ContainedViewType = .home
var body: some View {
VStack {
// custom header goes here
containedView()
}
}
func containedView() -> AnyView {
switch containedViewType {
case .home: return AnyView(HomeView())
case .categories: return AnyView(CategoriesView())
...
}
}
如果您指定 ViewBuilder
的 return 类型,您似乎不需要将 switch 语句提取到单独的函数中。例如:
Group { () -> Text in
switch status {
case .on:
return Text("On")
case .off:
return Text("Off")
}
}
Note: You can also return arbitrary view types if you wrap them in
AnyView
and specify that as the return type.
更新:SwiftUI 2 现在支持函数构建器中的 switch 语句,https://github.com/apple/swift/pull/30174
添加到 Nikolai 的答案中,该答案编译了开关但不使用转换,这是他的示例的一个版本,它支持转换。
struct RootView: View {
@State var containedViewType: ContainedViewType = .home
var body: some View {
VStack {
// custom header goes here
containedView()
}
}
func containedView() -> some View {
switch containedViewType {
case .home: return AnyView(HomeView()).id("HomeView")
case .categories: return AnyView(CategoriesView()).id("CategoriesView")
...
}
}
请注意已添加到每个 AnyView 的 id(...)
。这允许 SwiftUI 识别其视图层次结构中的视图,从而允许它正确应用过渡动画。
不使用 AnyView()。我将使用一堆 if 语句并在我的枚举中实现协议 Equatable 和 CustomStringConvertible 以检索我的关联值:
var body: some View {
ZStack {
Color("background1")
.edgesIgnoringSafeArea(.all)
.onAppear { self.viewModel.send(event: .onAppear) }
// You can use viewModel.state == .loading as well if your don't have
// associated values
if viewModel.state.description == "loading" {
LoadingContentView()
} else if viewModel.state.description == "idle" {
IdleContentView()
} else if viewModel.state.description == "loaded" {
LoadedContentView(list: viewModel.state.value as! [AnimeItem])
} else if viewModel.state.description == "error" {
ErrorContentView(error: viewModel.state.value as! Error)
}
}
}
我将使用结构分隔我的视图:
struct ErrorContentView: View {
var error: Error
var body: some View {
VStack {
Image("error")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 100)
Text(error.localizedDescription)
}
}
}
您可以使用包装视图
struct MakeView: View {
let make: () -> AnyView
var body: some View {
make()
}
}
struct UseMakeView: View {
let animal: Animal = .cat
var body: some View {
MakeView {
switch self.animal {
case .cat:
return Text("cat").erase()
case .dog:
return Text("dog").erase()
case .mouse:
return Text("mouse").erase()
}
}
}
}
您可以将枚举与 @ViewBuilder
一起使用,如下所示...
清除枚举
enum Destination: CaseIterable, Identifiable {
case restaurants
case profile
var id: String { return title }
var title: String {
switch self {
case .restaurants: return "Restaurants"
case .profile: return "Profile"
}
}
}
现在在查看文件中
struct ContentView: View {
@State private var selectedDestination: Destination? = .restaurants
var body: some View {
NavigationView {
view(for: selectedDestination)
}
}
@ViewBuilder
func view(for destination: Destination?) -> some View {
switch destination {
case .some(.restaurants):
CategoriesView()
case .some(.profile):
ProfileView()
default:
EmptyView()
}
}
}
如果您想使用与 NavigationLink 相同的大小写...您可以按如下方式使用它
struct ContentView: View {
@State private var selectedDestination: Destination? = .restaurants
var body: some View {
NavigationView {
List(Destination.allCases,
selection: $selectedDestination) { item in
NavigationLink(destination: view(for: selectedDestination),
tag: item,
selection: $selectedDestination) {
Text(item.title).tag(item)
}
}
}
}
@ViewBuilder
func view(for destination: Destination?) -> some View {
switch destination {
case .some(.restaurants):
CategoriesView()
case .some(.profile):
ProfileView()
default:
EmptyView()
}
}
}
在 switch
中提供 default
语句为我解决了这个问题:
struct RootView : View {
@State var containedView: ContainedView = .home
var body: some View {
// custom header goes here
switch containedView {
case .home: HomeView()
case .categories: CategoriesView()
...
default: EmptyView()
}
}
}