148

I'm trying to conditionally hide a DatePicker in SwiftUI. However, I'm having any issue with mismatched types:

var datePicker = DatePicker($datePickerDate)
if self.showDatePicker {
    datePicker = datePicker.hidden()
}

In this case, datePicker is a DatePicker<EmptyView> type but datePicker.hidden() is a _ModifiedContent<DatePicker<EmptyView>, _HiddenModifier>. So I cannot assign datePicker.hidden() to datePicker. I've tried variations of this and can't seem to find a way that works. Any ideas?

UPDATE

You can unwrap the _ModifiedContent type to get the underlying type using it's content property. However, this doesn't solve the underlying issue. The content property appears to just be the original, unmodified date picker.

Jake
  • 13,097
  • 9
  • 44
  • 73

12 Answers12

183

The simplest and most common way to hide a view is like the following:

struct ContentView: View {
    @State private var showText = true

    var body: some View {
        VStack {
            Button("Toggle text") {
                showText.toggle()
            }

            if showText {
                Text("Hello World!")
            }
        }
    }
}

This removes the Text view from the hierarchy when showText equals false. If you wish to have an option to preserve the space or want it as a modifier, see below.


I created an extension, so you can use a modifier, like so to hide the view:

Text("Hello World!")
    .isHidden(true)

Or for complete removal:

Text("Label")
    .isHidden(true, remove: true)

The extension below is also available on GitHub here if you want to use Swift Packages: GeorgeElsham/HidingViews.


Here is the code to create the View modifier:

I recommend you use this code in its own file (remember to import SwiftUI):

extension View {
    /// Hide or show the view based on a boolean value.
    ///
    /// Example for visibility:
    ///
    ///     Text("Label")
    ///         .isHidden(true)
    ///
    /// Example for complete removal:
    ///
    ///     Text("Label")
    ///         .isHidden(true, remove: true)
    ///
    /// - Parameters:
    ///   - hidden: Set to `false` to show the view. Set to `true` to hide the view.
    ///   - remove: Boolean value indicating whether or not to remove the view.
    @ViewBuilder func isHidden(_ hidden: Bool, remove: Bool = false) -> some View {
        if hidden {
            if !remove {
                self.hidden()
            }
        } else {
            self
        }
    }
}
George
  • 25,988
  • 10
  • 79
  • 133
  • 3
    If used to hide an object in a Form, it will still show a tappable blank view. – atulkhatri Dec 14 '19 at 19:08
  • 1
    This is great! Have you published this anywhere? I'd love to share it but it's odd sharing a StackOverflow answer instead of a code repo. – Ky - Jan 15 '20 at 04:37
  • 1
    @BenLeggiero It can now be found [here](https://github.com/George-J-E/HidingViews). – George Jan 15 '20 at 08:26
  • @atulkhatri if you want to completely remove the view, you can edit the body function to return an `EmptyView()` in the group instead of the `content`. I would like to suggest this as minor tweak/flag for the modifier as wel @George_E. – Bram Feb 18 '20 at 14:36
  • Is this possible to animate? – Antony Stubbs Jun 29 '20 at 07:55
  • @AntonyStubbs Yes! Where you toggle the value for if the content is hidden, you wrap it in `withAnimation`. E.g. in a button: `withAnimation { hidden.toggle() }`. – George Jun 29 '20 at 08:44
  • Thanks @George_E! I've also discovered that if you do indeed wrap the view in a simple conditional statement as in @Jake's answer, you can apply animation modifier to the property binding is in the conditional and it will actually animate as well. See here: https://www.hackingwithswift.com/quick-start/swiftui/how-to-animate-changes-in-binding-values – Antony Stubbs Jun 29 '20 at 09:23
  • @JarWarren I don’t know much about licences, but you are completely free to use this code and always will be – George Oct 25 '20 at 10:15
  • 1
    I imported your package (Xcode 12.3) and it works great! – Frederick C. Lee Jan 05 '21 at 22:10
  • You need to be slightly careful with extensions like this. The `if/else` statement means that `ViewBuilder` assigns different structural identities to the output of if vs else. i.e. The hidden view != the shown view. Therefore animations might not behave as expected, and `NS/UIViewRepresentable` items will be regenerated when hidden/shown. – Giles Oct 14 '22 at 13:31
  • I agree with Giles. Please see [my answer](https://stackoverflow.com/a/76240992/14840926) for an extension that deals with that particular issue and talks about animation problems when removing the view too. – Curious Jorge May 13 '23 at 04:00
180

✅ The correct and Simplest Way:

You can set the alpha instead, this will preserve the layout space of the view too and does not force you to add dummy views like the other answers:

.opacity(isHidden ? 0 : 1)

Demo

Demo


Cleaner Way! - Extend original hidden modifier:

Also, you can implement a custom function to get the visibility state as an argument:

extension View {
    func hidden(_ shouldHide: Bool) -> some View {
        opacity(shouldHide ? 0 : 1)
    }
}

Now just pass the bool to the modifier:

DatePicker($datePickerDate)
    .hidden(showDatePicker)

Note that unlike the original behavior of the hidden modifier, both of these methods preserve the frame of the hiding view.


⛔️ Don't use bad practices !!!

All other answers (including the accepted answer by @Jake) use branches instead of dependent code that cause a performance hit.

Branch example: (from WWDC)

Branch

✅ Dependent Code example:

Dependent Code example

Returning logical SAME view for different states causes the SwiftUI to render engine to re-render and initial a view again and cause a performance hit! (see more at this WWDC session)

Mojtaba Hosseini
  • 95,414
  • 31
  • 268
  • 278
  • 3
    I like this answer because it will still preserve the layout space of the view. Using `.hidden()` is good for that too but it doesn't seem optimum to have to use an if else condition with the `.hidden()` to preserve the space. – Mark Moeykens Sep 30 '19 at 17:12
  • 1
    I agree, keeping the view around in the hierarchy is a better practice IMO because if you decide to introduce animations, the animation engine will have something to work with instead of "popping" the view into existence. – Jack Feb 13 '20 at 19:58
  • 10
    Why is preserving layout space a 'GOOD' thing? – zaitsman Jul 07 '21 at 06:08
  • 4
    @zaitsman Depends on situation, but in some cases it prevents unwanted content jumps – Roman Banks Aug 28 '21 at 17:50
  • 14
    "Don't use bad practices !!!" is a bit extreme considering branching is needed to dynamically add/remove a view. If not can you provide a way that doesn't use branching? – Patrick Oct 04 '21 at 17:24
  • 8
    One thing to watch out for here is that changing the opacity will not update the accessibility hierarchy correctly. Using .hidden() will preserve the layout while removing the item from the accessibility hierarchy. – Chris Vasselli Jan 04 '22 at 22:33
  • 1
    @Patrick you can use `frame` modifier to rip out the extra space if needed. Also, SwiftUI `View`s are different that `UIVIew`s! Removing them when not needed is not a good option in most situations, otherwise, the render engine will rerender the entire View hierarchy!!!! – Mojtaba Hosseini Jan 27 '22 at 15:11
  • 1
    That doesn’t solve the problem if you’ve got a VStack with spacing. There will be spacing above and below the hidden frame. – Patrick Jan 28 '22 at 17:56
  • 1
    Speaking of bad practices, isn't that extension misleading as named? I'm pretty sure certain lifecycle events like onAppear and onDisappear are not going to fire with that extension as one might otherwise expect. – Manny Feb 12 '22 at 14:57
  • @Chris Vasselli - that's a good point. My experiments also show that an opacity of 0.1 disables gesture handling but larger opacity values allow gestures. If you want, say, a grayed out view but want to disable taps, it would be best to use a conditional modifier with that sets hidden() instead of opacity. – tdl Mar 22 '22 at 16:07
  • isHidden != isInvisible – Cublax Jun 11 '22 at 03:05
  • 1
    Both examples have a branch. The second just uses a ternary: https://www.hackingwithswift.com/sixty/3/7/the-ternary-operator. – Ben Thomas Jun 16 '22 at 21:36
  • 1
    using `frame()` can further collapse the clear space when hidden: `viewToHide.opacity(isShown ? 1.0 : 0.0).frame(maxHeight: isShown ? nil : 0)` – Hudon Sep 07 '22 at 20:23
  • This is an example of a very bad practice with nice icons though. It resolves the problem only on visual layer and makes thousands of problems. Like have you heard of accessibility when you've tried to downgrade other proper solutions??? – Ivan Ičin Nov 28 '22 at 13:38
  • 1
    Just watch [the WWDC session](https://developer.apple.com/wwdc21/10022) by Apple. It's not my opinion. It's the original SwiftUI creators' recommendation! @IvanIčin And **yes**, I have heard of the accessibility and there is no issue with any of these methods. **You** should optimize your app for the accessibility ;) – Mojtaba Hosseini Nov 28 '22 at 14:14
  • @MojtabaHosseini I appreciate the link, I'll take a look. However I do know that SwiftUI is based on view recycling and that having branching may have negative influence on that. The problem is that it has rarely any meaningful performance effect and the code you suggested is unreadable which has a meaningful effect. On every know framework it would cause accessibility artefacts, but I haven't checked on SwiftUI in particular it could be that they are doing something different than everyone else. – Ivan Ičin Nov 30 '22 at 13:11
  • Hi @MojtabaHosseini, you approach is very efficient. However, there is a point that I do not have an information: what is the difference between dependent code and branching in theory in terms of performance? could you refer to any reading material? – musakokcen Feb 16 '23 at 09:58
  • 2
    @musakokcen there’s a wwdc video that explains this nicely. I think it was called demystifying SwiftUI. The performance hit is that branching code will fully tear down your view and replace it with another. Because the views in each branch have a different identity. – Xaxxus Mar 07 '23 at 15:56
  • @IvanIčin the performance hit isn’t bad. But you can have weird side effects if the view in your branching code creates its own state. For example if you have an state object initializing in your branched view, it will be deinit and re initialized when you show/hide the view. This resets the state of that view. – Xaxxus Mar 07 '23 at 16:00
  • 1
    Link to "Demystify SwiftUI": https://developer.apple.com/videos/play/wwdc2021/10022/ – soundflix Jun 22 '23 at 06:49
  • Needed to replace my view with another and then return immediately to my original. Like putting a dialog up, I didn't want to lose my original context. Setting/reseting the opacity to hide the main view THEN using ZStack() to show whatever I wanted to replace it was perfect. I could return immediately to the context I left without any significant code, and it is instant. – Mythlandia Sep 03 '23 at 03:48
67

Rather than dynamically setting a variable and using it in my view, I found that I was able to hide or show the date picker this way:

struct ContentView : View {
    @State var showDatePicker = true
    @State var datePickerDate: Date = Date()

    var body: some View {
        VStack {
            if self.showDatePicker {
                DatePicker($datePickerDate)
            } else {
                DatePicker($datePickerDate).hidden()
            }
        }
    }
}

Or, optionally, not including the date picker instead of hiding it:

struct ContentView : View {
    @State var showDatePicker = true
    @State var datePickerDate: Date = Date()

    var body: some View {
        VStack {
            if self.showDatePicker {
                DatePicker($datePickerDate)
            }
        }
    }
}
Jake
  • 13,097
  • 9
  • 44
  • 73
31

Here is the simple way to Show/Hide view in SwiftUI.

  1. Add @State variable:

    @State private var showLogo = false
    
  2. Add condition like below:

    VStack {
        if showLogo {
            Image(systemName: "house.fill")
                .resizable()
                .frame(width: 100, height: 100, alignment: .center)
                .foregroundColor(Color("LightGreyFont"))
                .padding(.bottom, 20)
        }
        Text("Real State App")
            .font(Font.custom("Montserrat-Regular", size: 30))
    }.padding(.vertical, 25)
    
  3. Change state of your @State variable to Show/Hide the view like below:

    Button(action: {
        withAnimation{
            self.showLogo.toggle()
        }
    
    }, label: {
        Text("Login").font(.system(size: 20, weight: .medium, design: .default))
            .frame(minWidth: 0, maxWidth: .infinity, maxHeight: 50)
            .foregroundColor(Color("BlackFont"))
            .cornerRadius(10)
    
    })
    

If you want to preserve space you can also use .opacity(showLogo ? 1 : 0) modifier.

enter image description here

iVarun
  • 6,496
  • 2
  • 26
  • 34
  • 5
    This should be the accepted answer! It is best practice for a declarative interface! Thanks! – gundrabur Nov 01 '20 at 19:18
  • ```if showLogo == true ``` Camparison to boolean seems suspicious. Also looks like using branches in view code considered to be a bad provtice. – Ilya Apr 12 '22 at 01:59
19

Edit Nov 4 2021

I now prefer another approach over the one in my original answer (below):

There are two possible solutions depending on if you want to keep the original space occupied or make the other views take the space of the one that's hidden.

Keep the space

DatePicker("Choose date", selection: $datePickerDate)
    .opacity(showDatePicker ? 1 : 0)

Even if we are adjusting just opacity here, touching the space where the DatePicker should be when it's hidden doesn't open the calendar.

Don't keep the space

if showDatePicker {
    DatePicker("Choose date", selection: $datePickerDate)
}

Original answer

For whoever needs it in the future, I created a ViewModifier which takes a Bool as parameter so you can bind a boolean value to show and hide the view declaratively by just setting your showDatePicker: Bool variable.

All code snippets require import SwiftUI.

The ViewModifier:

struct Show: ViewModifier {
    let isVisible: Bool

    @ViewBuilder
    func body(content: Content) -> some View {
        if isVisible {
            content
        } else {
            content.hidden()
        }
    }
}

The function:

extension View {
    func show(isVisible: Bool) -> some View {
        ModifiedContent(content: self, modifier: Show(isVisible: isVisible))
    }
}

And you can use it like this:

var datePicker = DatePicker($datePickerDate)
                     .show(isVisible: showDatePicker)
CristinaTheDev
  • 1,952
  • 2
  • 17
  • 25
  • 2
    Since `Show` does not mutate `isVisible`, it does not need to be binding, or `var`. You can just declare it as normal `let isVisible: Bool`, drop the `$`, and SwiftUI will still re-create the view upon changes. – Aviel Gross Oct 27 '21 at 19:33
  • @AvielGross you're right, thank you! I edited my original answer. I still struggled with the new paradigm back then. – CristinaTheDev Nov 04 '21 at 17:22
  • 2
    no worries! It also took me a WHILE to wrap my head around this! SwiftUI is almost like learning to program all over again (: – Aviel Gross Nov 04 '21 at 20:19
6

Command-click the view in question and select the Make Conditional option in Beta 5. I did this on one of my views (LiftsCollectionView), and it generated the following:

    if suggestedLayout.size.height > 150 {
      LiftsCollectionView()
    } else {
      EmptyView()
    }
Justin Ekins
  • 61
  • 1
  • 1
  • `EmptyView()` is the point. It actually erases existence of the view where `hidden()` simply make is transparent but still exists. – eonil Jul 25 '20 at 11:37
  • For "Don't keep the space" I don't see any other solution other than the if..else condition. – Mahmud Ahsan Aug 01 '23 at 06:34
6

I had the same problem, and I solved it in the following way:

Note: I use binding to hide and/or show dynamically.

1 - Create Modifier

struct HiddenModifier: ViewModifier{
    var isHide:Binding<Bool>
    
    func body(content: Content) -> some View {
        if isHide.wrappedValue{
            content
                .hidden()
        }
        else{
            content
        }
    }
}

2 - Add Modifier to view:

extension View{
    func hiddenModifier(isHide:Binding<Bool>) -> some View{
        return self.modifier(HiddenModifier(isHide: isHide))
    }
}

3 - Use Modifier

struct CheckHiddenView: View {
    @State var hide:Bool = false
    
    var body: some View {
        VStack(spacing: 24){
            Text("Check Hidden")
                .font(.title)
            
            RoundedRectangle(cornerRadius: 20)
                .fill(Color.orange)
                .frame(width: 150, height: 150, alignment: .center)
                .hiddenModifier(hide: $hide)
            
            Button {
                withAnimation {
                    hide.toggle()
                }
                
            } label: {
                Text("Toggle")
            }
            .buttonStyle(.bordered)
            
        }
    }
}

Test

Check Swift hidden

Ever CR
  • 351
  • 3
  • 7
4

The following custom modifier works as .hidden() does by both hiding the view and disabling interaction with it.

ViewModifier and View extension func -

import SwiftUI

fileprivate struct HiddenIfModifier: ViewModifier {
  var isHidden: Bool
  
  init(condition: Bool) {
    self.isHidden = condition
  }
  
  func body(content: Content) -> some View {
    content
      // Conditionally changing the parameters of modifiers
      // is more efficient than conditionally applying a modifier
      // (as in Cristina's ViewModifier implementation).
      .opacity(isHidden ? 0 : 1)
      .disabled(isHidden)
  }
}

extension View {
    /// Hides a view conditionally.
    /// - Parameters:
    ///   - condition: Decides if `View` is hidden.
    /// - Returns: The `View`, hidden if `condition` is `true`.
    func hidden(if condition: Bool) -> some View {
        modifier(HiddenIfModifier(condition: condition))
    }
}

Use -

DatePicker($datePickerDate)
  .hidden(if: !self.showDatePicker)

Note - Conditionally applying a modifier is inefficient because swift sees the unmodified and modified views as different types. This causes the view (and it's state) to be destroyed and rebuilt every time the condition changes. This can become an issue for data heavy views like List. Conditionally changing the parameters of modifiers doesn't cause this issue.

Zach Foster
  • 157
  • 1
  • 9
2

You also have the opacity modifier on any View:

ActivityIndicator(tint: .black)
   .opacity(self.isLoading ? 1.0 : 0.0)
PiKey
  • 259
  • 2
  • 6
2

The bad part of the above solution .isHidden(true, remove: true) is onAppear callback will not be called when remove = true.

.opacity(isHidden ? 0 : 1) is definitely the right way.

LiangWang
  • 8,038
  • 8
  • 41
  • 54
1

The following also works even without a placeholder view or calling hidden (iOS13.1 and Swift 5)

struct Foo: View {
    @State var condition: Bool

    var body: some View {
        if self.condition {
            Text("Hello")
        }
    }
}

It's hard to know exactly without peeking at the @ViewBuilder implementation, but when evaluating a conditional, it seems that we are getting an EmptyView if it fails by default.

So this is equivalent to some of the answers here, but it's simpler.

FranMowinckel
  • 4,233
  • 1
  • 30
  • 26
1

Please review Apple's article Choosing the right way to hide a view.

Contrary to what many other answers here suggest, Apple's article states that a conditional is the correct solution.

The exception is when you want to preserve an empty space where the hidden view used to be. In that case, using .opacity(0) is the correct solution.

.hidden() is only suggested if the visibility of the view is always off. I suspect that's equivalent to .opacity(0), perhaps simply being a clearer expression of intent for exactly the same solution under the hood.

If you do want a modifier to make it convenient to do this, I would offer the following tweak on other people's work:

extension View {
    /// Hide or show a view based on a boolean value.
    ///
    /// Example for hiding while reclaiming space:
    ///
    ///     Text("Label")
    ///         .isHidden(true)
    ///
    /// Example for hiding, but leaving a gap where the hidden item was:
    ///
    ///     Text("Label")
    ///         .isHidden(true, remove: false)
    ///
    /// - Parameters:
    ///   - hidden: whether to hide the view.
    ///   - remove: whether you want to reclaim the space taken by the hidden view.
    @ViewBuilder func isHidden(_ hidden: Bool, remove: Bool = true) -> some View {
        if remove {
            if !hidden {
                self
            }
        } else {
            self.opacity(hidden ? 0 : 1)
        }
    }
}

Arranging the conditional that way has the advantage that, at least in the case where remove is set to false, the structural identity of the hidden view is consistent through animation of the hidden parameter.

On the other hand, if remove is set to true, there is no view in the hidden case, so it's not possible to maintain structural identity. So if you are animating the hidden parameter and remove is true, you might run into some unpleasant animation (depending on where SwiftUI determines is the best guess for the geometry of the removed view). To solve that, the answer, in theory, is to use .matchedGeometryEffect (see this to explore this technique). But I have found matchedGeometryEffect to be buggy, so for now, I would recommend that you turn animation off, and avoid all these headaches.

Curious Jorge
  • 354
  • 1
  • 9