动态隐藏 SwiftUI 视图

154

我试图在SwiftUI中有条件地隐藏一个DatePicker。然而,我遇到了类型不匹配的问题:

var datePicker = DatePicker($datePickerDate)
if self.showDatePicker {
    datePicker = datePicker.hidden()
}
在这种情况下,datePicker 是一个 DatePicker<EmptyView> 类型,但是 datePicker.hidden() 是一个 _ModifiedContent<DatePicker<EmptyView>, _HiddenModifier>。因此我不能将 datePicker.hidden() 赋值给 datePicker。我尝试了各种变化,但好像找不到可行的方法。有什么想法吗?
更新:
您可以使用其 content 属性取消包装 _ModifiedContent 类型以获取基础类型。 但是,这并没有解决潜在的问题。 content 属性似乎只是原始的、未修改的日期选择器。
12个回答

188
✅ 正确且简单的方法:
你可以设置 alpha 值,这样可以保留视图的布局空间,而且不需要像其他答案那样添加虚拟视图。
.opacity(isHidden ? 0 : 1)

演示

Demo


更简洁的方式!- 扩展原始的hidden修饰符:
此外,您还可以实现一个自定义函数,将可见性状态作为参数传入:
extension View {
    func hidden(_ shouldHide: Bool) -> some View {
        opacity(shouldHide ? 0 : 1)
    }
}

现在只需将`bool`传递给修饰符即可。
DatePicker($datePickerDate)
    .hidden(showDatePicker)

请注意,与hidden修饰符的原始行为不同,这两种方法都保留了隐藏视图的框架。
⛔️ 不要使用不良实践!!!
所有其他答案(包括@Jake的被接受的答案)都使用分支而不是导致性能下降的依赖代码。
分支示例:(来自WWDC

Branch

✅ 依赖代码示例:

Dependent Code example

返回不同状态的相同视图会导致SwiftUI渲染引擎重新渲染并初始化视图,从而导致性能下降!(详见此WWDC会议

3
我喜欢这个答案,因为它仍会保留视图的布局空间。使用.hidden()也可以实现这一点,但是必须使用if else条件语句和.hidden()一起使用以保留空间,这似乎不是最优解。 - Mark Moeykens
12
保持布局空间为什么是一件“好事”? - zaitsman
4
取决于情况,但在某些情况下,它可以防止不必要的内容跳动。 - Roman Banks
16
考虑到需要动态地添加/删除视图,说“不要使用不良做法!!!"有些过激。如果可以的话,您能否提供一种不需要分支的方法? - Patrick
8
这里需要注意的一点是,更改不透明度不会正确更新可访问性层次结构。使用.hidden()将保留布局同时从可访问性层次结构中移除该项。 - Chris Vasselli
显示剩余17条评论

184

隐藏视图最简单、最常见的方法如下:

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

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

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

showText等于false时,这将从层次结构中删除Text视图。如果您希望保留空格或将其作为修饰符,请参见下文。


我创建了一个扩展,因此您可以使用修饰符来隐藏视图,如下所示:

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

或者完全删除:

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

如果您想使用Swift Packages,下面的扩展程序也可以在GitHub上找到:GeorgeElsham/HidingViews


以下是创建View修饰符的代码:

我建议您将此代码放在单独的文件中(记得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
        }
    }
}

3
如果用于在表单中隐藏一个对象,它仍会显示一个可点击的空白视图。 - atulkhatri
1
太棒了!你在任何地方发布过这个吗?我很想分享它,但分享一个 StackOverflow 的答案而不是代码仓库感觉有点奇怪。 - Ky -
1
@BenLeggiero 现在可以在这里找到它(https://github.com/George-J-E/HidingViews)。 - George
1
我导入了你的包(Xcode 12.3),它运行得非常好! - Frederick C. Lee
你需要对这样的扩展略加小心。if/else语句意味着ViewBuilder会为if和else的输出分配不同的结构标识。即隐藏的视图!=显示的视图。因此,动画可能无法按预期工作,并且当隐藏/显示时,NS/UIViewRepresentable项将被重新生成。 - Giles
显示剩余6条评论

67

我发现,与其在视图中动态设置变量并使用它,不如通过以下方式隐藏或显示日期选择器:

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

或者,可选地,不包括日期选择器,而不是隐藏它:

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

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

1
我创建了一个ViewModifier,我认为它更加清晰,可以在此链接查看。 - George
5
.hidden()的目的是隐藏一个元素,使其在页面上不可见。 - Michael Ozeryansky
@MichaelOzeryansky 不确定。我可能会使用第二个例子。 - Jake
33
@Eugene,我想知道为什么hidden()不接受布尔值。 - Michael Ozeryansky
2
@MichaelOzeryansky,是的,我也不知道,但我也觉得应该这样。 - Eugene
显示剩余3条评论

32

这是在SwiftUI中显示/隐藏视图的简单方法。

  1. 添加 @State 变量:

    @State private var showLogo = false
    
  2. 添加以下条件:

    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. 更改您的 @State 变量的状态以显示/隐藏视图,如下所示:

    Button(action: {
        withAnimation{
            self.showLogo.toggle()
        }
    
    }, label: {
        Text("登录").font(.system(size: 20, weight: .medium, design: .default))
            .frame(minWidth: 0, maxWidth: .infinity, maxHeight: 50)
            .foregroundColor(Color("BlackFont"))
            .cornerRadius(10)
    
    })
    
如果你想保留空间,你也可以使用.opacity(showLogo ? 1 : 0)修改器。

enter image description here


5
这应该是被接受的答案!这是声明式接口的最佳实践!谢谢! - gundrabur
if showLogo == true 比较布尔值似乎有点可疑。而且在视图代码中使用分支逻辑被认为是一种不良实践。 - Ilya

19

2021年11月4日更新

我现在更喜欢另一种方法,而不是我原来的答案(如下):

根据您想要保留原始空间还是使其他视图占用隐藏的空间,有两种可能的解决方案。

保留空间

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

即使我们只是在调整不透明度,当隐藏的DatePicker应该出现的位置被触摸时,也不会打开日历。

不要保留空间

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

原始答案

对于未来需要的人,我创建了一个ViewModifier,它以Bool作为参数,因此您可以将布尔值绑定到视图上以声明性地显示和隐藏视图,只需设置您的showDatePicker: Bool变量。

所有代码片段都需要import SwiftUI

ViewModifier

struct Show: ViewModifier {
    let isVisible: Bool

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

这个功能:

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

您可以像这样使用它:

var datePicker = DatePicker($datePickerDate)
                     .show(isVisible: showDatePicker)

2
由于Show不会改变isVisible,因此它不需要被绑定为var。您可以将其声明为普通的let isVisible: Bool,并且删除$,SwiftUI仍会在更改时重新创建视图。 - Aviel Gross
@AvielGross 你说得对,谢谢!我编辑了我的原始答案。当时我仍然在努力适应新的编程范式。 - Cristina De Rito
2
没关系!我也花了一段时间才理解这个!SwiftUI几乎就像重新学习编程一样 (: - Aviel Gross

6

我曾经遇到过同样的问题,我是通过以下方式解决的:

注意:我使用绑定(binding)来动态隐藏和/或显示。

1 - 创建修饰符(Modifier)

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

2 - 给视图添加修饰符:

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

3 - 使用修饰符

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

测试

检查Swift隐藏


最佳答案!这是正确的方式。 - Rob

6

在 Beta 5 中,按住 Command 键单击相关视图,然后选择“Make Conditional”选项。我在我的一个视图(LiftsCollectionView)上执行了这个操作,并生成了以下代码:

    if suggestedLayout.size.height > 150 {
      LiftsCollectionView()
    } else {
      EmptyView()
    }

EmptyView() 是关键点。它实际上抹掉了视图的存在,而 hidden() 只是使其透明但仍然存在。 - eonil
对于“不要保留空格”,我没有看到除了if..else条件之外的其他解决方案。 - Mahmud Ahsan

4
以下自定义修饰符像 .hidden() 一样工作,既隐藏视图又禁用与之交互。
ViewModifier 和 View 扩展函数-
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))
    }
}

使用 -

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

注意 - 有条件地应用修饰符是低效的,因为 Swift 将未修改和修改后的视图视为不同类型。这会导致视图(及其状态)在每次条件更改时被销毁并重建。对于像 List 这样的数据密集型视图,这可能成为问题。有条件地更改修饰符的参数不会导致此问题。

2
您可以在任何 View 上使用 opacity 修饰符:
ActivityIndicator(tint: .black)
   .opacity(self.isLoading ? 1.0 : 0.0)

2
以上解决方案的不好之处是,当 remove = true 时,onAppear 回调将不会被调用。 .opacity(isHidden ? 0 : 1) 明显是正确的方法。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接