Альтернатива оператору switch в блоке SwiftUI ViewBuilder?

⚠️ 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.

Итак, есть ли альтернативы переключению, чтобы я мог воспроизвести это поведение?

Не используйте переключатель в своем представлении, это не место для логики, сделайте функцию вне тела

Lu_ 24.06.2019 14:22

Я попытался сделать функцию, возвращающую someView, и переместить туда оператор switch, но на этот раз ошибка: «Функция объявляет непрозрачный тип возвращаемого значения, но операторы возврата в ее теле не имеют соответствующих базовых типов» :(

Nikolay Marinov 24.06.2019 14:36
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
80
2
37 564
8
Перейти к ответу Данный вопрос помечен как решенный

Ответы 8

Вы должны обернуть свой код в представление, например 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, но безуспешно. Группа тоже не работает.

Nikolay Marinov 24.06.2019 14:24

Возврат, с другой стороны, отбрасывает другие представления в View Builder. Я хочу иметь другие представления, кроме представления из оператора switch.

Nikolay Marinov 24.06.2019 14:29

Хорошо. Этого не было в вашем первоначальном вопросе. Я добавлю кое-что, касающееся этого случая.

LinusGeffarth 24.06.2019 15:09

Добавлен третий вариант. Не могу проверить это прямо сейчас, поэтому рассмотрим этот псевдокод. Хотя логика должна быть правильной.

LinusGeffarth 24.06.2019 15:51

Я уже пробовал это. Посмотрите на мой комментарий к вопросу (под предложением Lu_). :( И кстати, как ни странно, SwiftUI принимает несколько else if, но я так не пойду :D

Nikolay Marinov 24.06.2019 15:57

Я не уверен, почему я получаю так много голосов против этого ответа (6 на момент написания этого). Кто-нибудь хочет объяснить?

LinusGeffarth 18.01.2020 15:34

Ни один из этих подходов не компилируется для меня (конечно, вы ожидаете, что они скомпилируются).

opsb 09.02.2020 20:47
Ответ принят как подходящий

⚠️ 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 идеален! Мне было интересно, для чего это было. Спасибо!

kontiki 24.06.2019 18:22
ПРИМЕЧАНИЕ Анимации перехода, которые запускаются при добавлении или удалении представления из иерархии, похоже, не работают с переключателем. Даже если указано явно. Однако они работают с оператором IF.
kontiki 24.06.2019 18:38

У вас даже может быть необязательный AnyView, если вы хотите условно показать вид.

gnarlybracket 04.10.2019 16:50

Спасибо за это! Однако я хочу добавить, что вам даже не нужна вспомогательная функция. Просто обертывание AnyView делает свое дело!

hasen 20.11.2019 05:19

Обратите внимание, что AnyView() стирает тип и, следовательно, предотвращает работу некоторых оптимизаций производительности SwiftUI. В этой статье более подробно объясняется, почему: objc.io/blog/2019/11/05/static-types-in-swiftui

orospakr 26.11.2019 04:52

Влияет ли стирание типа AnyView на анимацию?

Zorayr 08.05.2020 21:12

@zorayn Судя по этому вопросу, может подойти: stackoverflow.com/questions/58160011/… . Одна вещь, на которую это точно влияет, — это производительность. SwiftUI должен выполнять дополнительную работу для обновления AnyView.

Nikolay Marinov 11.05.2020 17:45

Похоже, вам не нужно извлекать оператор 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 AnyView and 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 или представление внутри него?

Bonteq 07.06.2020 17:17

Просто чтобы перепроверить, поскольку это обновление DSL, мы все еще можем отправить код на iOS 13+, верно?

Zorayr 25.10.2020 01:06

За то, что не использовал 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 когда с соответствующим значением

AlexanderZ 01.11.2020 11:05

Вы можете сделать с оберткой 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()
      }
   }
}

Другие вопросы по теме