⚠️ 23 июня 2020 г. Редактировать: начиная с Xcode 12 операторы switch и if let будут поддерживаться в ViewBuilder!
Я пытался воспроизвести свое приложение с помощью 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.
Итак, есть ли альтернативы переключению, чтобы я мог воспроизвести это поведение?
Я попытался сделать функцию, возвращающую someView, и переместить туда оператор switch, но на этот раз ошибка: «Функция объявляет непрозрачный тип возвращаемого значения, но операторы возврата в ее теле не имеют соответствующих базовых типов» :(





Вы должны обернуть свой код в представление, например VStack или Group:
var body: some View {
Group {
switch containedView {
case .home: HomeView()
case .categories: CategoriesView()
...
}
}
}
или добавление возвращаемых значений должно работать:
var body: some View {
switch containedView {
case .home: return HomeView()
case .categories: return CategoriesView()
...
}
}
Однако лучшим способом решения этой проблемы было бы создание метода, возвращающего представление:
func nextView(for containedView: YourViewEnum) -> some AnyView {
switch containedView {
case .home: return HomeView()
case .categories: return CategoriesView()
...
}
}
var body: some View {
nextView(for: containedView)
}
Я пытался с VStack, но безуспешно. Группа тоже не работает.
Возврат, с другой стороны, отбрасывает другие представления в View Builder. Я хочу иметь другие представления, кроме представления из оператора switch.
Хорошо. Этого не было в вашем первоначальном вопросе. Я добавлю кое-что, касающееся этого случая.
Добавлен третий вариант. Не могу проверить это прямо сейчас, поэтому рассмотрим этот псевдокод. Хотя логика должна быть правильной.
Я уже пробовал это. Посмотрите на мой комментарий к вопросу (под предложением Lu_). :( И кстати, как ни странно, SwiftUI принимает несколько else if, но я так не пойду :D
Я не уверен, почему я получаю так много голосов против этого ответа (6 на момент написания этого). Кто-нибудь хочет объяснить?
Ни один из этих подходов не компилируется для меня (конечно, вы ожидаете, что они скомпилируются).
⚠️ 23 июня 2020 г. Редактировать: Начиная с Xcode 12, операторы switch и if let будут поддерживаться в ViewBuilder!
Спасибо за ответы, ребята. Я нашел решение на Форумы разработчиков Apple.
На него ответил Киль Гиллард. Решение состоит в том, чтобы извлечь переключатель в функцию, как предложили 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())
...
}
}
Ух ты! Трюк AnyView идеален! Мне было интересно, для чего это было. Спасибо!
У вас даже может быть необязательный AnyView, если вы хотите условно показать вид.
Спасибо за это! Однако я хочу добавить, что вам даже не нужна вспомогательная функция. Просто обертывание AnyView делает свое дело!
Обратите внимание, что AnyView() стирает тип и, следовательно, предотвращает работу некоторых оптимизаций производительности SwiftUI. В этой статье более подробно объясняется, почему: objc.io/blog/2019/11/05/static-types-in-swiftui
Влияет ли стирание типа AnyView на анимацию?
@zorayn Судя по этому вопросу, может подойти: stackoverflow.com/questions/58160011/… . Одна вещь, на которую это точно влияет, — это производительность. SwiftUI должен выполнять дополнительную работу для обновления AnyView.
Похоже, вам не нужно извлекать оператор switch в отдельную функцию, если вы укажете возвращаемый тип ViewBuilder. Например:
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
AnyViewand specify that as the return type.
Обновление: SwiftUI 2 теперь включает поддержку операторов switch в построителях функций, https://github.com/apple/swift/pull/30174
В дополнение к ответу Николая, в котором переключатель компилируется, но не работает с переходами, вот версия его примера, которая поддерживает переходы.
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")
...
}
}
Обратите внимание на id(...), который был добавлен к каждому AnyView. Это позволяет SwiftUI идентифицировать представление в своей иерархии представлений, что позволяет ему правильно применять анимацию перехода.
Нужно ли анимировать AnyView или представление внутри него?
Просто чтобы перепроверить, поскольку это обновление DSL, мы все еще можем отправить код на iOS 13+, верно?
За то, что не использовал AnyView(). Я буду использовать несколько операторов if и реализовывать протоколы Equatable и CustomStringConvertible в своем Enum для получения связанных значений:
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)
}
}
}
просто замечание к вашему комментарию в коде: вы можете использовать if case .loading = viewModel.state когда с соответствующим значением
Вы можете сделать с оберткой View
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()
}
}
}
}
Вы можете использовать enum с @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()
}
}
}
Предоставление оператора default в switch решило это для меня:
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()
}
}
}
Не используйте переключатель в своем представлении, это не место для логики, сделайте функцию вне тела