Alternative to switch statement in SwiftUI ViewBuilder block? - swift

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

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

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.

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.

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

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

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

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

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

Related

Nesting of several NavigationLink in a NavigationStack causes the loss of animation and breaks the backtracking

SwiftUI 4.0 introduces a new NavigationStack view.
Let's consider this simple structure.
struct Item: Identifiable, Hashable {
static let sample = [Item(), Item(), Item()]
let id = UUID()
}
When a NavigationLink is nested in another one, the navigation loses its animation and the backtracking takes directly to the root. Did I miss something, or is this a bug?
struct ItemDetailView: View {
let item: Item
var body: some View {
Text(item.id.uuidString)
}
}
struct ItemListView: View {
var body: some View {
List(Item.sample) { item in
NavigationLink(item.id.uuidString, value: item)
}
}
}
struct ExploreView: View {
var body: some View {
List {
Section {
NavigationLink {
ItemListView()
} label: {
Text("Items")
}
}
}
.navigationTitle("Explore")
.navigationDestination(for: Item.self) { item in
ItemDetailView(item: item)
}
}
}
struct ContentView: View {
var body: some View {
NavigationStack {
ExploreView()
}
}
}
Thanks!
Found the solution thanks to #Asperi's comment.
First, create a Hashable enum containing the destinations.
enum Destination: Hashable {
case items
var view: some View {
switch self {
case .items:
return ItemListView()
}
}
var title: LocalizedStringKey {
switch self {
case .items:
return "Items"
}
}
}
Next, use the new NavigationLink initializer.
NavigationLink(Destination.items.title, value: Destination.items)
And finally, add a new .navigationDestination modifier to catch all Destination values.
.navigationDestination(for: Destination.self) { destination in
destination.view
}

Unable to access #EnvironmentObject from a child view constructed with a function

I'm trying to pass an EnvironmentObject to child views but am having no luck with the following code.
struct ContentView: View {
enum MyViews: Int, CaseIterable {
case introView
case viewOne
case viewTwo
var activeView: AnyView {
switch self.rawValue {
case 0: return AnyView(IntroView())
case 1: return AnyView(ViewOne())
case 2: return AnyView(ViewTwo())
default: return AnyView(IntroView())
}
}
#State var pageIndex = 0
func content() -> MyViews? {
let newPage = MyViews.init(rawValue: pageIndex)
return newPage
}
}
var body: some View {
Group {
content()?.activeView // Problem appears to lie here
}
.environmentObject(userData)
}
If I replace content()?.activeView with a simple view e.g. TestView() then I'm able to successfully print out the userData variable in TestView(). But as it stands, I get a crash when trying to access userData in ViewOne(), even when ViewOne is identical to TestView().
Any idea what I'm doing wrong?
The problem is that you need to pass your EnvironmentObject manually via init() to your viewModel. It won't be injected automatically. Here is a approach how to do it
func getActiveView(userData: UserData) -> AnyView {
switch self.rawValue {
case 0: return AnyView(IntroView(userData: userData))
case 1: return AnyView(ViewOne())
default: return AnyView(IntroView(userData: userData))
}
}
In your View of ContentView call the function and pass the userData
ZStack {
content()?
.getActiveView(userData: userData)
.environmentObject(userData)
}
IntroView and ViewModel take userData as parameter
class AListViewModel: ObservableObject {
var userData: UserData
init(userData: UserData) {
self.userData = userData
}
func myFunc(){
print("myVar: \(userData.myVar)")
}
}
struct IntroView: View {
#EnvironmentObject var userData: UserData
#ObservedObject var someListVM : AListViewModel
init(userData: UserData) {
someListVM = AListViewModel(userData: userData)
}

Picker in NavigationView: No refresh when custom binding changes

I'm trying to save an enum value to UserDefaults. Therefor I created a custom binding. The UserDefault part works fine but unfortunately the value in the NavigationView does not get refreshed after changing the value with the Picker.
How can I get the chosen weather condition to be displayed?
PS: If not creating additional complexity, I would like to keep the enum Weather of type Int.
import SwiftUI
enum Weather: Int, CaseIterable {
case rain
case sun
case clouds
}
func getWeatherText(weather: Weather) -> String {
switch weather {
case .rain: return "Rain"
case .sun: return "Sun"
case .clouds: return "Clouds"
}
}
struct ContentView: View {
let currentWeather = Binding<Weather>(
get: {
(Weather(rawValue: UserDefaults.standard.integer(forKey: "weather")) ?? .sun)
},
set: {
UserDefaults.standard.set($0.rawValue, forKey: "weather")
}
)
let weathers = Weather.allCases
var body: some View {
NavigationView{
Form{
Picker("Weather", selection: currentWeather) {
ForEach(weathers, id: \.self) { w in
Text(getWeatherText(weather: w)).tag(w)
}
}
}
}
}
}
Thanks to the comment of Asperi, the solution is simple: replacing my custom Binding with #AppStorage. This is the result:
import SwiftUI
enum Weather: Int, CaseIterable {
case rain
case sun
case clouds
}
func getWeatherText(weather: Weather) -> String {
switch weather {
case .rain: return "Rain"
case .sun: return "Sun"
case .clouds: return "Clouds"
}
}
struct ContentView: View {
#AppStorage("weather") var currentWeather: Weather = Weather.clouds
let weathers = Weather.allCases
var body: some View {
NavigationView{
Form{
Picker("Weather", selection: $currentWeather) {
ForEach(weathers, id: \.self) { w in
Text(getWeatherText(weather: w)).tag(w)
}
}
}
}
}
}

SwiftUI can not return View on function

I'm trying return View based selected menu item on function. But it throws error:
Protocol 'View' can only be used as a generic constraint because it has Self or associated type requirements.
There my code:
enum MenuItem {
case Main
case Report
}
struct Menu: View {
#State var activeItem: MenuItem = .Main
private func getActiveView() -> View {
switch activeItem {
case .Main:
return DashboardView()
case .Report:
return ReportView()
}
}
var body: some View {
...
getActiveView()
...
}
}
struct DashboardView: View {
var body: some View {
Text("Contact")
}
}
struct ReportView: View {
var body: some View {
Text("Contact")
}
}
Im new on SwiftUI. Any ideas how to return View?
SwiftUI 2
Here is a solution tested with Xcode 12b / iOS 14
struct Menu: View {
#State var activeItem: MenuItem = .Main
// make function ViewBuilder
#ViewBuilder
private func getActiveView() -> some View {
switch activeItem {
case .Main:
DashboardView() // don't use 'return' as it disables ViewBuilder
case .Report:
ReportView()
}
}
var body: some View {
getActiveView()
}
}
SwiftUI gives us a type-erased wrapper called AnyView that we can return.
Tested Solution:
struct Menu: View {
#State var activeItem: MenuItem = .Main
func getActiveView() -> some View {
switch activeItem {
case .Main:
return AnyView(DashboardView())
case .Report:
return AnyView(ReportView())
}
}
var body: some View {
getActiveView()
}
}
Note: type-erased wrapper effectively forces Swift to forget about what specific
type is inside the AnyView, allowing them to look like they are the
same thing. This has a performance cost, though, so don’t use it
often.
For more information you can refer to the this cool article: https://www.hackingwithswift.com/quick-start/swiftui/how-to-return-different-view-types

Associated enum state in SwiftUI

How can I use an associated enum as a #State variable in an if statement in SwiftUI?
struct ProfileView: View {
#State private var choice = Choice.simple
private enum Choice {
case simple
case associated(Int)
}
var body: some View {
if choice == .simple {
Text("Simple")
}
}
}
The compiler reports this error:
Protocol 'Equatable' requires that 'ProfileView.Choice' conform to 'Equatable'
Here is fixed variant. Tested with Xcode 11.4.
struct ProfileView: View {
#State private var choice = Choice.simple
private enum Choice: Equatable {
case simple
case associated(Int)
}
var body: some View {
Group {
if choice == .simple {
Text("Simple")
} else {
Text("Other")
}
}
}
}
You need to use if case to check if an enum variable matches a certain case.
var body: some View {
if case .simple = choice {
return Text("Simple")
} else {
return Text("Not so simple")
}
}
If you actually want to use the associated value to be displayed, I'd suggest using a switch to cover all enum cases.
var body: some View {
let text: String
switch choice {
case .simple:
text = "Simple"
case .associated(let value):
text = "\(value)"
}
return Text(text)
}