具有双向绑定的自定义SwiftUI视图

时间:2020-02-18 16:14:41

标签: ios swiftui combine

我正在努力实现一个自定义视图,该视图可以将Binding作为参数并对该值进行双向更新。

所以基本上我正在实现我的自定义滑块,并希望其初始化程序像这样:

MySlider(value: <Binding<Float>)


我正在努力的事情:

  1. 如何订阅绑定值的远程更新,以便可以更新视图的状态?
  2. 有没有什么好方法可以将Binding与@State属性绑定?

到目前为止,这是我目前的实现方式,

struct MySlider: View {

    @Binding var selection: Float?
    @State private var selectedValue: Float?

    init(selection: Binding<Float?>) {
        self._selection = selection

        // https://stackoverflow.com/a/58137096
        _selectedValue = State(wrappedValue: selection.wrappedValue)
    }

    var body: some View {
         HStack(spacing: 3) {
             ForEach(someValues) { (v) in
                 Item(value: v,
                      isSelected: v == self.selection)
                     .onTapGesture {
                         // No idea how to do that other way so I don't have to set it twice
                         self.selection = v
                         self.selectedValue = v
                    }
             }
         }
    }
}

修改1: 我想我的问题是底层模型对象来自Core Data,并且不属于任何观察其变化的SwiftUI视图。该模型对象归UIKit ViewController所有,我只传递了绑定到SwiftUI视图还不够。 我现在的解决方案是将模型对象也传递给SwiftUI视图,以便可以将其标记为@ObservedObject

struct MySlider<T>: View where T: ObservableObject {

    @ObservedObject var object: T
    @Binding var selection: Float?

    var body: some View {
        return ScrollView(.horizontal, showsIndicators: false) {
            HStack(spacing: 3) {
                ForEach(values) { (v) in
                    Item(value: v,
                         isSelected: v == self.selection)
                        .onTapGesture {
                            self.selection = v
                    }
                }
            }
        }
    }
}

2 个答案:

答案 0 :(得分:0)

@Binding的定义本质上是与基础数据项的双向连接,例如另一个视图所拥有的@State变量。如Apple所述:

使用绑定在视图及其基础模型之间创建双向连接。

如果是绑定,则SwiftUI将在值更改时自动更新其视图;因此(回答第一个问题),您无需进行任何订阅即可更新您的自定义视图-这将自动发生。

类似地(关于您的第二个问题),因为绑定是另一个视图的状态,所以您也不应将其声明为视图的状态,我也不会相信这是可能的。状态应该是视图的纯粹内部值(Apple强烈建议将所有@State属性都声明为private)。

这一切都与苹果在发布SwiftUI时强调的“单一真相”概念有关:该绑定已经@State的父视图才是拥有信息的主体,因此您的视图也不应该声明为状态。

对于您的代码,我想您需要做的就是删除第二个状态属性,因为这不是必需的。只要确保您传递的绑定在任何拥有您的自定义视图的父视图中都是@State属性,然后使用$语法将其传递来创建该绑定即可。 This article会在需要时更详细地介绍这个想法。

struct MySlider: View {

    @Binding var selection: Float?

    init(selection: Binding<Float?>) {
        self._selection = selection
    }

    var body: some View {
         HStack(spacing: 3) {
             ForEach(someValues) { (v) in
                 Item(value: v, isSelected: v == self.selection)
                     .onTapGesture {
                         self.selection = v
                    }
             }
         }
    }
}

答案 1 :(得分:0)

我的回答可能有点晚了,但是我偶然发现了一个类似的问题。所以这就是我的解决方法。

struct Item : Identifiable {
    var id : Int
    var isSelected : Bool
}

class MySliderObserver : ObservableObject {
   
   @Published var values = [Item]()
    
    init() {
        values.append(Item(id: 0, isSelected: false))
        values.append(Item(id: 1, isSelected: false))
        values.append(Item(id: 2, isSelected: false))
    }
    
    func toggleSelectForItem(id:Int) -> Void {
        guard let index = values.firstIndex(where: { $0.id == id }) else {
            return
        }

        var item = values[index]
        item.isSelected.toggle()
        values[index] = item
    }
}

struct MySlider: View {

    @EnvironmentObject var observer : MySliderObserver

    var body: some View {
        return ScrollView(.horizontal, showsIndicators: false) {
            VStack(spacing: 3) {

                ForEach(self.observer.values) { v in
                    Text("Selected: \(v.isSelected ? "true" : "false")").onTapGesture {
                        self.observer.toggleSelectForItem(id: v.id)
                    }
                }
            }
        }
    }
}

,在SceneDelegate中,您需要设置EnvironmentObject,例如:

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        let contentView = MySlider()

        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(rootView: contentView.environmentObject(MySliderObserver()))
            self.window = window
            window.makeKeyAndVisible()
        }
    }