Dynamically hiding view in SwiftUI
SwiftSwiftuiSwift Problem Overview
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.
Swift Solutions
Solution 1 - Swift
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
}
}
}
Solution 2 - Swift
✅ 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
hidden
modifier:
Cleaner Way! - Extend original 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:
✅ 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)
Solution 3 - Swift
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)
}
}
}
}
Solution 4 - Swift
Here is the simple way to Show/Hide view in SwiftUI.
-
Add
@State
variable:@State var showLogo = false
-
Add condition like below:
VStack { if showLogo == true { 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)
-
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) })
Solution 5 - Swift
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
.
ViewModifier
:
The 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)
Solution 6 - Swift
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()
}
Solution 7 - Swift
You also have the opacity
modifier on any View
:
ActivityIndicator(tint: .black)
.opacity(self.isLoading ? 1.0 : 0.0)
Solution 8 - Swift
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.
Solution 9 - Swift
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.