如何有选择地在SwiftUI中传递绑定?

时间:2019-11-21 20:50:57

标签: swiftui

我有一个内部管理@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
    }

}

谢谢!

6 个答案:

答案 0 :(得分:1)

您要实现的主要问题是,当索引由父级处理时,您的View需要对其进行@Binding,但是当其处理索引本身时,则需要@State。有两种可能的解决方案。

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

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。

答案 1 :(得分:1)

(我认为)最好的解决方案是使用包装器视图自定义绑定

这将给您两个选择:

  1. 用父级视图绑定(子级)视图,以便父级和子级都可以更改值
  2. 请勿将子级View与父级绑定,这样只有子级将在内部保存该值。

实际上是很棘手的事情。

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

父视图(与子视图相同,除了子视图)

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

答案 2 :(得分:0)

如果使用目标注入进行绑定不是目标,则内部仅绑定一件事-内部@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 :(得分:0)

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

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

           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 :(得分:0)

根据我的经验,将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之间共享索引。

答案 5 :(得分:-1)

想法是为可重用视图使用@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))
        }
    }
}

请注意,我添加了一些按钮,操作和帮助器以使其易于显示,但是您当然可以根据需要更改所有内容