138

I'm trying to figure out the correct way to conditionally include a view with swiftui. I wasn't able to use the if directly inside of a view and had to use a stack view to do it.

This works but there seems like there would be a cleaner way.

var body: some View {
    HStack() {
        if keychain.get("api-key") != nil {
            TabView()
        } else {
            LoginView()
        }
    }
}
KYDronePilot
  • 527
  • 3
  • 12
Michael St Clair
  • 5,937
  • 10
  • 49
  • 75
  • 1
    "Can someone explain how to read this declaration?" It's a standard generic. What's the confusion? – matt Jun 09 '19 at 19:09
  • `ConditionalContent` seems to me a either/or type of struct that gets generated from the compiler when interpreting a `@ViewBuilder` block. I think that's how our `ifs/elses` inside Groups. Stacks, etc are translated. I think so because it yields a `View`. In your case, that `if/else` gets translated to a `ConditionalContent`. – Matteo Pacini Jun 09 '19 at 19:12
  • I’m new to swift, so I’m not sure how that actually translates to usage – Michael St Clair Jun 09 '19 at 19:16
  • @MichaelStClair you are using it already, if my intuition is correct :) you can only use if/else at the moment, according to the WWDC videos. Your `if/else` gets translated by the compiler to `ConditionalContent` (most likely). – Matteo Pacini Jun 09 '19 at 19:20
  • Ok that makes sense. So to the main question, is the way I’m doing it above considered best practice, or is there a different type of view that would be considered better for this type of situation (as the first layer view)? – Michael St Clair Jun 09 '19 at 19:22
  • 4
    @MichaelStClair we're all newbies when it comes to `SwiftUI`, so it will take some time to define a `best practice`. Code looks good, so go for it! An improvement you could do: have a state in the view to decide whether to show the `TabView` or `LoginView`, and then mutate that state via a view model - via a `Binding`. – Matteo Pacini Jun 09 '19 at 19:32
  • 3
    If the `HStack { ... }` is only used to provide an “outer group” (to make the if-else compile) then you can also use `Group { ... }` instead. – Martin R Jun 09 '19 at 19:36
  • IMO the conditional stuff could come before you even present a view. Having business logic in your view could get messy (keyword could). Wherever you want present a view you do your business logic and then if else for example. Im not saying this is wrong but I would probably avoid using if else in my views as much as possible. – andromedainiative Jun 09 '19 at 19:53
  • 3
    I've just verified that `if/else` in a `@ViewBuilder` block yields a `ConditionalStatement` at compiler level: https://i.imgur.com/VtI4yLg.png. – Matteo Pacini Jun 09 '19 at 20:04
  • @Matteo Pacini: I saw the imag you provided. Please can you tell me why I get "Cannot infer contextual base in reference to member 'constant'" at the "presentation(.constant.... line? Do I miss some code above? – iPadawan Jul 22 '20 at 23:01

15 Answers15

189

The simplest way to avoid using an extra container like HStack is to annotate your body property as @ViewBuilder, like this:

@ViewBuilder
var body: some View {
    if user.isLoggedIn {
        MainView()
    } else {
        LoginView()
    }
}
Yurii Kotov
  • 1,912
  • 1
  • 6
  • 5
  • Using this way caused my animation stop working. The if statement in my case it on a boolean that other view toggle with animation in order to show/hide the view inside the if statement by adding to it a transition modifier. – EdiZ Nov 19 '19 at 16:30
  • @IanWarburton This might help you: [What enables SwiftUI's DSL?](https://stackoverflow.com/q/56434549/8697793) – pawello2222 Nov 02 '20 at 20:51
  • Thanks a lot! This problem have been sticking me for an entire morning. – WeZZard Jan 01 '21 at 03:40
  • 2
    was using a ternary operator which it didn't like ‍♂️ – Paul Fitzgerald Jan 04 '21 at 10:53
  • One significant issue of using "if" in this way is performance degradation. The view won't be able to load within time if there is 5-6 number of "if-else" conditions. It will show you to break the view into multiple pieces but dividing the view won't help much. I found the solution of @gabriellanata a big performance up for an extreme level of If-else situation. – Md Razon Hossain Mar 11 '21 at 05:05
79

I needed to embed a view inside another conditionally, so I ended up creating a convenience if function:

extension View {
   @ViewBuilder
   func `if`<Content: View>(_ conditional: Bool, content: (Self) -> Content) -> some View {
        if conditional {
            content(self)
        } else {
            self
        }
    }
}

This does return an AnyView, which is not ideal but feels like it is technically correct because you don't really know the result of this during compile time.

In my case, I needed to embed the view inside a ScrollView, so it looks like this:

var body: some View {
    VStack() {
        Text("Line 1")
        Text("Line 2")
    }
    .if(someCondition) { content in
        ScrollView(.vertical) { content }
    }
}

But you could also use it to conditionally apply modifiers too:

var body: some View {
    Text("Some text")
    .if(someCondition) { content in
        content.foregroundColor(.red)
    }
}

UPDATE: Please read the drawbacks of using conditional modifiers before using this: https://www.objc.io/blog/2021/08/24/conditional-view-modifiers/

gabriellanata
  • 3,946
  • 2
  • 21
  • 27
37

You didn't include it in your question but I guess the error you're getting when going without the stack is the following?

Function declares an opaque return type, but has no return statements in its body from which to infer an underlying type

The error gives you a good hint of what's going on but in order to understand it, you need to understand the concept of opaque return types. That's how you call the types prefixed with the some keyword. I didn't see any Apple engineers going deep into that subject at WWDC (maybe I missed the respective talk?), which is why I did a lot of research myself and wrote an article on how these types work and why they are used as return types in SwiftUI.

What’s this “some” in SwiftUI?

There is also a detailed technical explanation in another

Stackoverflow post on opaque result types

If you want to fully understand what's going on I recommend reading both.


As a quick explanation here:

General Rule:

Functions or properties with an opaque result type (some Type)
must always return the same concrete type.

In your example, your body property returns a different type, depending on the condition:

var body: some View {
    if someConditionIsTrue {
        TabView()
    } else {
        LoginView()
    }
}

If someConditionIsTrue, it would return a TabView, otherwise a LoginView. This violates the rule which is why the compiler complains.

If you wrap your condition in a stack view, the stack view will include the concrete types of both conditional branches in its own generic type:

HStack<ConditionalContent<TabView, LoginView>>

As a consequence, no matter which view is actually returned, the result type of the stack will always be the same and hence the compiler won't complain.


Supplemental:

There is actually a view component SwiftUI provides specifically for this use case and it's actually what stacks use internally as you can see in the example above:

ConditionalContent

It has the following generic type, with the generic placeholder automatically being inferred from your implementation:

ConditionalContent<TrueContent, FalseContent>

I recommend using that view container rather that a stack because it makes its purpose semantically clear to other developers.

Mischa
  • 15,816
  • 8
  • 59
  • 117
  • I had tried using conditional content but got an error, how exaclty would I use that? `Cannot convert value of type '() -> ()' to expected argument type 'ConditionalContent<_, _>.Storage'` – Michael St Clair Jun 09 '19 at 22:18
  • `var body: some View { ConditionalContent() { if userData.apiKey != nil { TabView() } else { LoginView() } } }` – Michael St Clair Jun 09 '19 at 22:19
  • I honestly don't know why that doesn't work. Tried it myself, ran into the same error. They way I understand it, `ConditionalContent` _should_ be exactly the right tool here, given its documentation: _View content that shows one of two possible children._ I read a few posts on Twitter mentioning several bugs that still exist in SwiftUI. Maybe this is one of them. For now, I'd go with stacks or groups then or hope that someone else can provide a good answer for how to use `ConditionalContent` properly. – Mischa Jun 09 '19 at 22:37
  • 1
    The `ConditionalContent` is indeed the right tool to use, but if you look closer, you will see that it has no public initializer, so you shouldn't use it directly, but the `ViewBuilder` as a couple of methods that actually returns a `ConditionContent`. My guess is that using a `if ` statement is the only way to achieve that. – rraphael Jun 11 '19 at 08:52
  • @rraphael can you please give an example? – Seb Aug 25 '19 at 14:49
  • 8
    Does `ConditionalContent` still exist? Your link returns a 404. – speg May 20 '20 at 16:28
  • Poof! It's gone! Never use internal APIs! – SacWebDeveloper May 09 '21 at 08:06
13

Anyway, the issue still exists. Thinking mvvm-like all examples on that page breaks it. Logic of UI contains in View. In all cases is not possible to write unit-test to cover logic.

PS. I am still can't solve this.

UPDATE

I am ended with solution,

View file:

import SwiftUI


struct RootView: View {

    @ObservedObject var viewModel: RatesListViewModel

    var body: some View {
        viewModel.makeView()
    }
}


extension RatesListViewModel {

    func makeView() -> AnyView {
        if isShowingEmpty {
            return AnyView(EmptyListView().environmentObject(self))
        } else {
            return AnyView(RatesListView().environmentObject(self))
        }
    }
}
Mike Glukhov
  • 1,758
  • 19
  • 18
  • 1
    Have tried so many of the other solution, but this was the only one that worked for me. Wrapping the views inside the if in a AnyView. – Philip Aarseth Dec 11 '19 at 18:42
  • 4
    In MVVM originally developed for WPF, View Model is an abstraction of View so I don't think your `makeView()`, which makes a specific view, should belong to View Model. View shoudn't include domain logic but it can include presentation logic. You can just put `makeView()` into `RootView`. – Manabu Nakazawa Apr 26 '20 at 16:17
  • @ManabuNakazawa the only reason why I put it here is do NOT include SwiftUI to Unit-test's target. 'You can just put' - yes, this example was just an example and final version has more abstraction on specific view and vm. – Mike Glukhov Apr 27 '20 at 08:50
6

Based on the comments I ended up going with this solution that will regenerate the view when the api key changes by using @EnvironmentObject.

UserData.swift

import SwiftUI
import Combine
import KeychainSwift

final class UserData: BindableObject  {
    let didChange = PassthroughSubject<UserData, Never>()
    let keychain = KeychainSwift()

    var apiKey : String? {
        get {
            keychain.get("api-key")
        }
        set {
            if let newApiKey : String = newValue {
                keychain.set(newApiKey, forKey: "api-key")
            } else {
                keychain.delete("api-key")
            }

            didChange.send(self)
        }
    }
}

ContentView.swift

import SwiftUI

struct ContentView : View {

    @EnvironmentObject var userData: UserData

    var body: some View {
        Group() {
            if userData.apiKey != nil {
                TabView()
            } else {
                LoginView()
            }
        }
    }
}
Michael St Clair
  • 5,937
  • 10
  • 49
  • 75
  • In Xcode 11 beta 6, when using `if let`, I get a compilation error: `Closure containing control flow statement cannot be used with function builder 'ViewBuilder'`, this might be relevant: https://medium.com/q42-engineering/swiftui-optionals-ead04edd439f – Sajjon Aug 23 '19 at 16:13
4

Another approach using ViewBuilder (which relies on the mentioned ConditionalContent)

buildEither + optional

import PlaygroundSupport
import SwiftUI

var isOn: Bool?

struct TurnedOnView: View {
    var body: some View {
        Image(systemName: "circle.fill")
    }
}

struct TurnedOffView: View {
    var body: some View {
        Image(systemName: "circle")
    }
}

struct ContentView: View {
    var body: some View {
        ViewBuilder.buildBlock(
            isOn == true ?
                ViewBuilder.buildEither(first: TurnedOnView()) :
                ViewBuilder.buildEither(second: TurnedOffView())
        )
    }
}

let liveView = UIHostingController(rootView: ContentView())
PlaygroundPage.current.liveView = liveView

(There's also buildIf, but I couldn't figure out its syntax yet. ¯\_(ツ)_/¯)


One could also wrap the result View into AnyView

import PlaygroundSupport
import SwiftUI

let isOn: Bool = false

struct TurnedOnView: View {
    var body: some View {
        Image(systemName: "circle.fill")
    }
}

struct TurnedOffView: View {
    var body: some View {
        Image(systemName: "circle")
    }
}

struct ContentView: View {
    var body: AnyView {
        isOn ?
            AnyView(TurnedOnView()) :
            AnyView(TurnedOffView())
    }
}

let liveView = UIHostingController(rootView: ContentView())
PlaygroundPage.current.liveView = liveView

But it kinda feels wrong...


Both examples produce the same result:

playground

backslash-f
  • 7,923
  • 7
  • 52
  • 80
3

I chose to solve this by creating a modifier that makes a view "visible" or "invisible". The implementation looks like the following:

import Foundation
import SwiftUI

public extension View {
    /**
     Returns a view that is visible or not visible based on `isVisible`.
     */
    func visible(_ isVisible: Bool) -> some View {
        modifier(VisibleModifier(isVisible: isVisible))
    }
}

fileprivate struct VisibleModifier: ViewModifier {
    let isVisible: Bool

    func body(content: Content) -> some View {
        Group {
            if isVisible {
                content
            } else {
                EmptyView()
            }
        }
    }
}

Then to use it to solve your example, you would simply invert the isVisible value as seen here:

var body: some View {
    HStack() {
        TabView().visible(keychain.get("api-key") != nil)
        LoginView().visible(keychain.get("api-key") == nil)
    }
}

I have considered wrapping this into some kind of an "If" view that would take two views, one when the condition is true and one when the condition is false, but I decided that my present solution is both more general and more readable.

Steven W. Klassen
  • 1,401
  • 12
  • 26
  • Note that I have now added this solution to my "KSSCore" library available to the public on GitHub at https://github.com/klassen-software-solutions/KSSCore/blob/master/Sources/KSSUI/ViewExtension.swift – Steven W. Klassen Aug 24 '20 at 15:05
  • Note that I have refactored the above KSSCore to separate out the UI and non-UI items. The code is now available at https://github.com/klassen-software-solutions/KSSCoreUI/blob/master/Sources/KSSSwiftUI/ViewExtension.swift – Steven W. Klassen Aug 09 '21 at 13:49
3

If the error message is

Closure containing control flow statement cannot be used with function builder 'ViewBuilder'

Just hide the complexity of the control flow from the ViewBuilder:

This works:

struct TestView: View {
    func hiddenComplexControlflowExpression() -> Bool {
        // complex condition goes here, like "if let" or "switch"
        return true
    }
    var body: some View {
        HStack() {
            if hiddenComplexControlflowExpression() {
                Text("Hello")
            } else {
                Image("test")
            }
            
            if hiddenComplexControlflowExpression() {
                Text("Without else")
            }
        }
    }
}
pkamb
  • 33,281
  • 23
  • 160
  • 191
Gerd Castan
  • 6,275
  • 3
  • 44
  • 89
3

Extension with the condition param works well for me (iOS 14):

import SwiftUI

extension View {
   func showIf(condition: Bool) -> AnyView {
       if condition {
           return AnyView(self)
       }
       else {
           return AnyView(EmptyView())
       }

    }
}

Example usage:

ScrollView { ... }.showIf(condition: shouldShow)
SašaM
  • 398
  • 1
  • 6
2

Previous answers were correct, however, I would like to mention, you may use optional views inside you HStacks. Lets say you have an optional data eg. the users address. You may insert the following code:

// works!!
userViewModel.user.address.map { Text($0) }

Instead of the other approach:

// same logic, won't work
if let address = userViewModel.user.address {
    Text(address)
}

Since it would return an Optional text, the framework handles it fine. This also means, using an expression instead of the if statement is also fine, like:

// works!!!
keychain.get("api-key") != nil ? TabView() : LoginView()

In your case, the two can be combined:

keychain.get("api-key").map { _ in TabView() } ?? LoginView()

Using beta 4

gujci
  • 1,238
  • 13
  • 21
2

I extended @gabriellanata's answer for up to two conditions. You can add more if needed. You use it like this:

    Text("Hello")
        .if(0 == 1) { $0 + Text("World") }
        .elseIf(let: Int("!")?.description) { $0 + Text($1) }
        .else { $0.bold() }

The code:

extension View {
    func `if`<TrueContent>(_ condition: Bool, @ViewBuilder  transform: @escaping (Self) -> TrueContent)
        -> ConditionalWrapper1<Self, TrueContent> where TrueContent: View {
            ConditionalWrapper1<Self, TrueContent>(content: { self },
                                                   conditional: Conditional<Self, TrueContent>(condition: condition,
                                                                                               transform: transform))
    }

    func `if`<TrueContent: View, Item>(`let` item: Item?, @ViewBuilder transform: @escaping (Self, Item) -> TrueContent)
        -> ConditionalWrapper1<Self, TrueContent> {
            if let item = item {
                return self.if(true, transform: {
                    transform($0, item)
                })
            } else {
                return self.if(false, transform: {
                    transform($0, item!)
                })
            }
    }
}


struct Conditional<Content: View, Trans: View> {
    let condition: Bool
    let transform: (Content) -> Trans
}

struct ConditionalWrapper1<Content: View, Trans1: View>: View {
    var content: () -> Content
    var conditional: Conditional<Content, Trans1>

    func elseIf<Trans2: View>(_ condition: Bool, @ViewBuilder transform: @escaping (Content) -> Trans2)
        -> ConditionalWrapper2<Content, Trans1, Trans2> {
            ConditionalWrapper2(content: content,
                                conditionals: (conditional,
                                               Conditional(condition: condition,
                                                           transform: transform)))
    }

    func elseIf<Trans2: View, Item>(`let` item: Item?, @ViewBuilder transform: @escaping (Content, Item) -> Trans2)
        -> ConditionalWrapper2<Content, Trans1, Trans2> {
            let optionalConditional: Conditional<Content, Trans2>
            if let item = item {
                optionalConditional = Conditional(condition: true) {
                    transform($0, item)
                }
            } else {
                optionalConditional = Conditional(condition: false) {
                    transform($0, item!)
                }
            }
            return ConditionalWrapper2(content: content,
                                       conditionals: (conditional, optionalConditional))
    }

    func `else`<ElseContent: View>(@ViewBuilder elseTransform: @escaping (Content) -> ElseContent)
        -> ConditionalWrapper2<Content, Trans1, ElseContent> {
            ConditionalWrapper2(content: content,
                                conditionals: (conditional,
                                               Conditional(condition: !conditional.condition,
                                                           transform: elseTransform)))
    }

    var body: some View {
        Group {
            if conditional.condition {
                conditional.transform(content())
            } else {
                content()
            }
        }
    }
}

struct ConditionalWrapper2<Content: View, Trans1: View, Trans2: View>: View {
    var content: () -> Content
    var conditionals: (Conditional<Content, Trans1>, Conditional<Content, Trans2>)

    func `else`<ElseContent: View>(@ViewBuilder elseTransform: (Content) -> ElseContent) -> some View {
        Group {
            if conditionals.0.condition {
                conditionals.0.transform(content())
            } else if conditionals.1.condition {
                conditionals.1.transform(content())
            } else {
                elseTransform(content())
            }
        }
    }

    var body: some View {
        self.else { $0 }
    }
}
Tomatrow
  • 51
  • 4
2

Here’s a very simple to use modifier which uses a boolean test to decide if a view will be rendered. Unlike other solutions posted here it doesn’t rely on the use of ÀnyView. This is how to use it:

var body: some View {
    VStack {
        FooView()
            .onlyIf(someCondition)
    }
}

This reads nicer than the default ifthen construct as it removes the additional indentation.

To replace an ifthenelse construct, this is the obvious solution:

var body: some View {
    VStack {
        FooView()
            .onlyIf(someCondition)

        BarView()
            .onlyIf(!someCondition)
    }
}

This is the definition of the onlyIf modifier:

struct OnlyIfModifier: ViewModifier {
    var condition: Bool

    func body(content: Content) -> some View {
        if condition {
            content
        }
    }
}

extension View {
    func onlyIf(_ condition: Bool) -> some View {
        modifier(OnlyIfModifier(condition: condition))
    }
}

Give it a try – it will surely clean up your code and improve overall readability.

Tom E
  • 1,530
  • 9
  • 17
1

How about that?

I have a conditional contentView, which either is a text or an icon. I solved the problem like this. Comments are very appreciated, since I don't know if this is really "swifty" or just a "hack", but it works:

    private var contentView : some View {

    switch kind {
    case .text(let text):
        let textView = Text(text)
        .font(.body)
        .minimumScaleFactor(0.5)
        .padding(8)
        .frame(height: contentViewHeight)
        return AnyView(textView)
    case .icon(let iconName):
        let iconView = Image(systemName: iconName)
            .font(.title)
            .frame(height: contentViewHeight)
        return AnyView(iconView)
    }
}
LukeSideWalker
  • 7,399
  • 2
  • 37
  • 45
  • The Type Checking required for this solution will cause an exponential increase in compile time per case. – Mike W. Jan 07 '22 at 01:14
1

Use Group instead of HStack

var body: some View {
        Group {
            if keychain.get("api-key") != nil {
                TabView()
            } else {
                LoginView()
            }
        }
    }
YodagamaHeshan
  • 4,996
  • 2
  • 26
  • 36
  • The documentation states: "For example, you can use groups to return large numbers of scenes or toolbar content instances, but not to return different scenes or toolbar content based on conditionals.", so it sounds like this isn't a sanctioned approach. – Ruben Martinez Jr. Oct 31 '22 at 23:01
  • @RubenMartinezJr. Hi, now things are different(Updated), the body is a viewbuilder , so we don't even need to provide Group to achieve this kind of scenario. – YodagamaHeshan Nov 01 '22 at 01:30
  • @RubenMartinezJr. check out how this is happening now with this great explanation https://developer.apple.com/wwdc21/10022 – YodagamaHeshan Nov 01 '22 at 01:30
0

If you want to navigate to two different views using NavigationLink, you can navigate using ternary operator.

    let profileView = ProfileView()
.environmentObject(profileViewModel())
.navigationBarTitle("\(user.fullName)", displayMode: .inline)
    
    let otherProfileView = OtherProfileView(data: user)
.environmentObject(profileViewModel())
.navigationBarTitle("\(user.fullName)", displayMode: .inline)
    
    NavigationLink(destination: profileViewModel.userName == user.userName ? AnyView(profileView) : AnyView(otherProfileView)) {
      HStack {
        Text("Navigate")
    }
    }
Gautam Vanama
  • 105
  • 3
  • 6