Alternative to switch statement in SwiftUI ViewBuilder block?

SwiftSwiftui

Swift Problem Overview


⚠️ 23 June 2020 Edit: From Xcode 12, both switch and if let statements will be supported in the ViewBuilder!

I’ve been trying to replicate an app of mine using SwiftUI. It has a RootViewController which, depending on an enum value, shows a different child view controller. As in SwiftUI we use views instead of view controllers, my code looks like this:

struct RootView : View {
   @State var containedView: ContainedView = .home

   var body: some View {
      // custom header goes here
      switch containedView {
         case .home: HomeView()
         case .categories: CategoriesView()
         ...
      }
   }
}

Unfortunately, I get a warning: >Closure containing control flow statement cannot be used with function builder ViewBuilder.

So, are there any alternatives to switch so I can replicate this behaviour?

Swift Solutions


Solution 1 - Swift

⚠️ 23 June 2020 Edit: From Xcode 12, both switch and if let statements will be supported in the ViewBuilder!

Thanks for the answers, guys. I’ve found a solution on Apple’s Dev Forums. It’s answered by Kiel Gillard. The solution is to extract the switch in a function as Lu_, Linus and Mo suggested, but we have to wrap the views in AnyView for it to work – like this:

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())
     ... 
  }
}

Solution 2 - Swift

Update: SwiftUI 2 now includes support for switch statements in function builders, https://github.com/apple/swift/pull/30174


Adding to Nikolai's answer, which got the switch compiling but not working with transitions, here's a version of his example that does support transitions.

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")
     ... 
  }
}

Note the id(...) that has been added to each AnyView. This allows SwiftUI to identify the view within it's view hierarchy allowing it to apply the transition animations correctly.

Solution 3 - Swift

It looks like you don't need to extract the switch statement into a separate function if you specify the return type of a ViewBuilder. For example:

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.

Solution 4 - Swift

You must wrap your code in a View, such as VStack, or Group:

var body: some View {
   Group {
       switch containedView {
          case .home: HomeView()
          case .categories: CategoriesView()
          ...
       }
   }
}

or, adding return values should work:

var body: some View {
    switch containedView {
        case .home: return HomeView()
        case .categories: return CategoriesView()
        ...
    }
}

The best-practice way to solve this issue, however, would be to create a method that returns a view:

func nextView(for containedView: YourViewEnum) -> some AnyView {
    switch containedView {
        case .home: return HomeView()
        case .categories: return CategoriesView()
        ...
    }
}

var body: some View {
    nextView(for: containedView)
}

Solution 5 - Swift

Providing default statement in the switch solved it for me:

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()
      }
   }
}

Solution 6 - Swift

You can do with a wrapper 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()
            }
        }
    }
}

Solution 7 - Swift

You can use enum with @ViewBuilder as follow ...

Declear enum

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"
    }
  }
  
}

Now in the View file

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()
    }
  }
}

If you want to use the same case with the NavigationLink ... You can use it as follow

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()
    }
  }
}

Solution 8 - Swift

For not using AnyView(). I will use a bunch of if statements and implement the protocols Equatable and CustomStringConvertible in my Enum for retrieving my associated values:

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)
        }
    }
}

And I will separate my views using a struct:

struct ErrorContentView: View {
    var error: Error

    var body: some View {
        VStack {
            Image("error")
                .resizable()
                .aspectRatio(contentMode: .fit)
                .frame(width: 100)
            Text(error.localizedDescription)
        }
    }
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionNikolay MarinovView Question on Stackoverflow
Solution 1 - SwiftNikolay MarinovView Answer on Stackoverflow
Solution 2 - SwiftopsbView Answer on Stackoverflow
Solution 3 - SwiftAvarioView Answer on Stackoverflow
Solution 4 - SwiftLinusGeffarthView Answer on Stackoverflow
Solution 5 - SwiftcedricbahirweView Answer on Stackoverflow
Solution 6 - Swiftonmyway133View Answer on Stackoverflow
Solution 7 - SwiftWahab Khan JadonView Answer on Stackoverflow
Solution 8 - SwiftRoy RodriguezView Answer on Stackoverflow