SwiftUI 如果让内部视图

Posted

技术标签:

【中文标题】SwiftUI 如果让内部视图【英文标题】:SwiftUI if let inside View 【发布时间】:2020-02-13 07:53:18 【问题描述】:

我想在 View 中添加一个 if let 语句。

@ObservedObject var person: Person?
 var body: some View 
      if person != nil 
        // this works
      
      if let p = person 
        // Compiler error
      
    

包含控制流语句的闭包不能与函数生成器“ViewBuilder”一起使用

【问题讨论】:

不,目前不支持。您可以在视图构建器中拥有的唯一一种控制流是 if...else 所以我应该强制转换这些情况? 使用强制转换不是一个好的解决方案。最好用 nil 检查并在 if-else 中处理 目前您可以强制转换/解包或调用不在@ViewBuilder 范围内的其他属性或函数,这样您就可以使用像if let 这样的正常控制流。 【参考方案1】:

是的,代码快照中请求的符号是不允许的,至少目前是这样,但是通过以下非常简单的方法 - 将控制流提取到函数中,可以实现预期的结果::: p>

var person: Person? // actually @ObservedObject does not allowed optional
var body: some View 
    VStack 
         if person != nil 
           // same as before
         
         personViewIfExists() // << just extract it in helper function
    


private func personViewIfExists() -> some View  // generates view conditionally
    if let p = person 
      return ExistedPersonView(person: p) // << just for demo
    

在某些情况下,可能还需要以下函数变体(尽管它产生相同的结果)

private func personViewIfExists() -> some View 
    if let p = person 
      return AnyView(ExistedPersonView(person: p))
    
    return AnyView(EmptyView())

【讨论】:

如果可能,应避免在 SwiftUI 中使用 AnyView,而应使用 @ViewBuilder 包装器。 以上评论见Demystify SwiftUI:developer.apple.com/videos/play/wwdc2021/10022【参考方案2】:

或者您可以创建自己的IfLet 视图生成器:

import SwiftUI

struct IfLet<Value, Content, NilContent>: View where Content: View, NilContent: View 

    let value: Value?
    let contentBuilder: (Value) -> Content
    let nilContentBuilder: () -> NilContent

    init(_ optionalValue: Value?, @ViewBuilder whenPresent contentBuilder: @escaping (Value) -> Content, @ViewBuilder whenNil nilContentBuilder: @escaping () -> NilContent) 
        self.value = optionalValue
        self.contentBuilder = contentBuilder
        self.nilContentBuilder = nilContentBuilder
    

    var body: some View 
        Group 
            if value != nil 
                contentBuilder(value!)
             else 
                nilContentBuilder()
            
        
    


extension IfLet where NilContent == EmptyView 

    init(_ optionalValue: Value?, @ViewBuilder whenPresent contentBuilder: @escaping (Value) -> Content) 
        self.init(optionalValue, whenPresent: contentBuilder, whenNil:  EmptyView() )
    

使用它,您现在可以执行以下操作:

var body: some View 
    IfLet(pieceOfData)  realData in
        // realData is no longer optional
    

如果您的选项是nil,想要回复吗?

var body: some View 
    IfLet(pieceOfData, whenPresent:  realData in
        // realData is no longer optional
        DataView(realData)
    , whenNil: 
        EmptyDataView()
    )

【讨论】:

【参考方案3】:

更简单的方法是只使用.map()

如果user 有值,它将显示UserProfile,如果user 为nil,则不显示:

var user: User? 
var body: some View 
  user.map  UserProfile(user: $0) 

如果你想为 nil 值提供一个默认视图,你可以使用 nil-coalescing 操作符:

var user: User? 
var body: some View 
  user.map  UserProfile(user: $0)  ?? UserProfile(user: .default) 

但是,这要求两个视图属于同一类型,实际上,如果它们属于同一类型,最好只写UserProfile(user: $0 ?? .default)

有趣的是两个视图不是同一类型的情况。您可以通过将它们包装在 AnyView 中来擦除它们的类型,但此时它确实有点麻烦且难以阅读:

var user: User? 
var body: some View 
  user.map  AnyView(UserProfile(user: $0))  ?? AnyView(Text("Not logged in"))

因此,在这种情况下,我首选的方法是使用自定义 IfLet 结构(与 Procrastin8 的名称相同,但实现和语法不同),它允许我编写类似这样的东西,我发现它的意图和易于打字。

var user: User? 
var body: some View 
  IfLet(user)  UserProfile(user: $0)  
    .else  Text("Not logged in") 

我的IfLet 组件的代码如下。

struct IfLet<Wrapped, IfContent> : View where IfContent: View 
  let optionalValue: Wrapped?
  let contentBuilder: (Wrapped) -> IfContent

  init(_ optionalValue: Wrapped?, @ViewBuilder contentBuilder: @escaping (Wrapped) -> IfContent) 
    self.optionalValue = optionalValue
    self.contentBuilder = contentBuilder
  

  var body: some View 
    optionalValue.map  contentBuilder($0) 
  

  func `else`<ElseContent:View>(@ViewBuilder contentBuilder: @escaping () -> ElseContent) -> some View 
    if optionalValue == nil 
      return AnyView(contentBuilder())
     else 
      return AnyView(self)
    
  

享受吧!

【讨论】:

以上是关于SwiftUI 如果让内部视图的主要内容,如果未能解决你的问题,请参考以下文章

如何在 SWIFTUI 正文视图中使用声明的变量

填充 SwiftUI 框架的内部?

SwiftUI 回调作为视图的参数

在父 swiftui 视图中,我如何知道内部视图 viewmodel void 方法中的状态何时发生变化?

SwiftUI:如何让滚动视图包含完整列表长度

在视图控制器中创建 Publishable 属性,将其传递给 SwiftUI 视图并监听视图控制器内部的更改?