【问题标题】:How to optionally pass in a Binding in SwiftUI?如何在 SwiftUI 中选择性地传入绑定?
【发布时间】:2019-11-21 20:50:57
【问题描述】:

我有一个内部管理一个跟踪当前索引的@State 变量的视图。所以像:

struct ReusableView: View {

    @State var index: Int = 0

    var body: some View {
        Text("The index is \(self.index)"
        // A button that changes the index
    }

}

此视图将在整个应用程序中重复使用。有时父视图需要访问索引,所以我这样重构它:

struct ParentView: View {

    @State var index: Int = 0

    var body: some View {
      ReusableView($index)
    }

}

struct ReusableView: View {

    @Binding var index: Int

    var body: some View {
        Text("The index is \(self.index)"
        // A button that changes the index
    }

}

问题

我不想强制父视图始终保持索引的状态。换句话说,我希望有选择地允许父视图负责状态变量,但默认使用可重用视图来维护状态,以防父视图不关心索引。

尝试

我尝试以某种方式初始化可重用视图上的绑定,以防父视图不提供绑定:

struct ReusableView: View {

    @Binding var index: Int

    init(_ index: Binding<Int>? = nil) {
        if index != nil {
            self._index = index
        } else {
            // TODO: Parent didn't provide a binding, init myself.
            // ?
        }
    }

    var body: some View {
        Text("The index is \(self.index)"
        // A button that changes the index
    }

}

谢谢!

【问题讨论】:

    标签: swift swiftui


    【解决方案1】:

    你想要实现的主要问题是,当索引由父级处理时,你的视图需要一个@Binding,但是当它处理索引本身时它需要@State。有两种可能的解决方案。

    如果视图可以在没有 index 属性时忽略它:

    struct ReusableView: View {
    
        @Binding var index: Int?
    
        init(_ index: Binding<Int?>) {
            self._index = index
        }
    
        init() {
           self._index = .constant(nil)
        }
    
        var body: some View {
            VStack {
                index.map { Text("The index is \($0)") }
            }
        }   
    }
    

    优点是它非常简单——只有两个初始化器,但是当它由 ResusableView 本身处理时,你不能更改索引的值(它是一个常量)。

    如果视图在没有索引属性的情况下无法忽略:

    struct ReusableView: View {
    
        private var content: AnyView
    
        init(_ index: Binding<Int>? = nil) {
            if let index = index {
                self.content = AnyView(DependentView(index: index))
            } else {
                self.content = AnyView(IndependentView())
            }
        }
    
        var body: some View {
            content
        }
    
        private struct DependentView: View {
    
            @Binding var index: Int
    
            var body: some View {
                Text("The index is \(index)")
            }
        }
    
        private struct IndependentView: View {
    
            @State private var index: Int = 0
    
            var body: some View {
                Text("The index is \(index)")
            }
        }
    
    }
    

    明显的优势是您有一个可以绑定到值或将其作为自己的状态管理的视图。如您所见,ReusableView 只是两个不同视图的包装器,一个管理自己的@State,一个绑定到其父视图的@State。

    【讨论】:

    • 我喜欢它,从所有帖子中似乎没有规范的解决方案,但这是我见过的最优雅的。
    • 您可以将 .constant(nil) 设为默认值以简化操作:init(_ index: Binding&lt;Int?&gt; = .constant(nil) ) {...} 无需使用第二个 init() 方法
    • 请注意IndependentView body 也可能只是DependentView 的一个实例,从而减少重复代码:var body: some View { DependentView(index: $index) }
    【解决方案2】:

    如果不是使用依赖注入进行绑定的目标,那么只有一件事要在内部绑定 - 内部@State。所以这里是使用内部绑定到内部状态的替代方法,你可以试试。

    struct ReusableView: View {
    
        @Binding private var externalIndex: Int
        @State private var internalIndex: Int = 0
        private var hasExternal = false
    
        init(_ index: Binding<Int>? = nil) {
            if index != nil {
                self._externalIndex = index!
                self.hasExternal = true
            } else {
                self._externalIndex = Binding<Int>(get: {0}, set: {_ in}) // initialization stub
            }
        }
    
        private var index: Binding<Int> {
            hasExternal ? $externalIndex : $internalIndex
        }
    
        var body: some View {
            VStack {
                Text("The index is \(self.index.wrappedValue)")
                // A button that changes the index
            }
        }
    
    }
    

    【讨论】:

      【解决方案3】:

      正如您建议更改 init,我认为您的方向是正确的。

      在 UI 世界中,当您尝试 init 某事时,人们更喜欢添加视图。因此,您只需要在父视图和原始可重用视图之间添加一个中间视图。您甚至不需要更改视图代码

                 struct ParentView: View {
                      @State var index: Int?
                      var body: some View {
                          ReusableView(index: $index)
                      }
                  }
      
                  struct ReusableView_Original: View {
                      @Binding var index: Int
                      var body: some View {
                          VStack{
                          Text("The index is \(self.index)")
                              Button.init("click", action:{
                                  self.index += 1
                              })}
                      }
                  }
      

      //中间视图如下。

                  struct ReusableView: View{
                      @Binding var index: Int?
                      var body: some View {
                          ReusableView_Original(index: Binding<Int>(get: {
                              return self.index == nil ? 0 : self.index!
                          }, set: { (int) in
                              self.index = int
                          }))
                      }
                  }
      

      您可以像这样扩展和测试结果:

         struct ParentView: View {
                      @State var index: Int? = 2
                      var body: some View {
                          VStack{
                          ReusableView(index: $index)
                          ReusableView(index: $index, useDefault:  true)
                          }
                      }
                  }
      
      
                  struct ReusableView: View{
                      @Binding var index: Int?
                      @State var useDefault = false
                      @State var defaultValue = 0
      
                      var body: some View {
                          ReusableView_Original(index: Binding<Int>(get: {
                              return self.useDefault ? self.defaultValue : self.index!
                          }, set: { (int) in
                              self.useDefault = false
                              self.index = int
                          }))
                      }
                  }
      

      【讨论】:

        【解决方案4】:

        这里最好的解决方案(在我看来)是带有 包装视图自定义绑定

        这会给你两个选择:

        1. 将您的(子)视图与父视图绑定,以便父视图和子视图都可以更改值
        2. 不要将子视图与父视图绑定,这样只有子视图会在内部保存值。

        其实很简单。

        这是您的示例,已更改为有效。 请记住,实际更改是包装视图,并且您必须从父(而不是子)调用包装视图。

        父视图(与您的相同,但子视图除外)

        struct ContentView: View {
            @State var index: Int? = nil
        
            var body: some View {
                VStack {
                    Text("Parent view state: \(index ?? 0)")
                    WrapperOfReusableView(index: self.$index)
                    // if you add  .constant(nil) it will use the internal storage
                }
            }
        }
        

        包装视图

        struct WrapperOfReusableView: View {
            // The external index
            @Binding var index: Int?
            // The internal index
            @State private var localIndex = 0
        
            var body: some View {
                // Custom binding
                let localIndexBinding = Binding(
                    get:{
                        // if the parent sends nil, use the internal property
                        self.index ?? self.localIndex
                    },
                    set: {
                        // Set both internal and external
                        self.localIndex = $0
                        self.index = $0
                    }
                )
                // return the actual View
                return ReusableView(index: localIndexBinding)
            }
        }
        

        实际视图(与您的相同)

        struct ReusableView: View {
            @Binding var index: Int
        
            var body: some View {
                VStack {
                    Text("The Reusable index is \(index)")
        
                    Button("Change Index") {
                        self.index = self.index + 1
                    }
                }
            }
        }
        

        【讨论】:

          【解决方案5】:

          我们的想法是为可重用视图设置一个@State,并在需要时将其与父级同步。因为每个视图应该只有一个状态管理器。但它可以与外部经理同步。

          因此,您可以将其设为 optional 并添加另一个内部 @State 以进行本地更改:

          struct ReusableView: View {
          
              @Binding var parentIndex: Int?
              @State private var defaultIndex = 0 { didSet { guard parentIndex != nil else { return }; parentIndex! += 1 } }
              var index: Int { parentIndex ?? defaultIndex }
          
              var body: some View {
                  VStack {
                      Text("The index is \(self.index)")
                      Button("Reuseable") { self.defaultIndex += 1 }
                  }
              }
          }
          

          那么当你不希望它像这样被父母处理时,你可以轻松地传递.constant(nil)

          struct ParentView: View {
          
              @State var index: Int? = 0
          
              var body: some View {
                  VStack {
                      Text("Parent: \(index!)")
                      ReusableView(parentIndex: $index)
                      Button("Parent") { self.index! += 1 }
                      Divider()
                      ReusableView(parentIndex: .constant(nil))
                  }
              }
          }
          

          请注意,我添加了一些按钮和操作以及帮助程序以使其可演示,但您当然可以随意更改它们

          【讨论】:

            【解决方案6】:

            根据我的经验,将index 包裹在ObservableObject 中会更干净、更方便。

            class IndexStore: ObservableObject {
                @Published index = 0
            }
            

            然后在你的ReusableView

            struct ReusableView: View {
            
                var indexStore = IndexStore()
            
                var body: some View {
                    Text("The index is \(self.indexStore.index)"
                    // A button that changes the index
                }
            
            }
            

            然后您可以将其实例化为 ReusableView() 用于“内部”索引,ReusableView(indexStore: parentIndexStore) 用于父索引,甚至可以在不同的ReusableView 之间共享索引。

            【讨论】:

            • 谢谢,虽然这可行,但我正在寻找绑定解决方案
            • 我学到了一个艰难的方法,你不应该强迫 SwiftUI 和 Combine 用于它不是为它设计的东西。
            猜你喜欢
            • 1970-01-01
            • 2020-04-04
            • 1970-01-01
            • 2018-03-14
            • 1970-01-01
            • 1970-01-01
            • 2016-05-11
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多