在 SwiftUI 中使用双向绑定为每个实例实时更改数据

Real-time data changes using two-way binding for each instance in SwiftUI

我想创建 Circle() 节点的实例,用户可以在其中点击并在屏幕上四处拖动它们。每次从其起始位置拉出一个 Circle() 节点时,都会在其位置创建一个新节点,从而允许用户创建任意数量的节点。

然后我想要为每个创建的实例获得屏幕位置的实时变化数据,但在我选择的不同视图中,所以我可以将它用于进一步的图形和效果。

如何从不同的视图访问每个单独实例的实时屏幕位置数据?

这是我要创建实例的子视图,访问 currentPosition 变量:

import SwiftUI

struct Child: View {
    @EnvironmentObject var settings: DataBridge
    @Binding var stateBinding: CGSize
    
    @State var isInitalDrag = true
    @State var isOnce = true
    
    @State var currentPosition: CGSize = .zero
    @State var newPosition: CGSize = .zero
    
    var body: some View {
        Circle()
            .frame(width: 50, height: 50)
            .foregroundColor(.blue)
            .offset(self.currentPosition)
            .gesture(
                DragGesture()
                    .onChanged { value in
                        
                        if self.isInitalDrag && self.isOnce {
                            
                            // Call function in ContentView here:
                            
                            self.isOnce = false
                        }
                        
                        self.currentPosition = CGSize(
                            width: CGFloat(value.translation.width + self.newPosition.width),
                            height: CGFloat(value.translation.height + self.newPosition.height)
                        )
                        
                        self.stateBinding = self.currentPosition
                    }
                    .onEnded { value in
                        self.newPosition = self.currentPosition
                        
                        self.isOnce = true
                        self.isInitalDrag = false
                    }
            )
    }
}

struct Child_Previews: PreviewProvider {
    static var previews: some View {
        Child(stateBinding: .constant(.zero))
    }
}

这是一种可能的方法(粗糙,只显示重要的代码)- 从子视图到父视图的数据可以通过视图首选项传递。

所以我们有首选项键来存储子 id 的字典作为键,它的位置作为值,父视图创建带有 id 的子视图,子视图在首选项中存储自己的位置,父视图在首选项更改时读取位置。

struct ViewPositionsKey: PreferenceKey {
    static var defaultValue = [Int: CGSize]()
    static func reduce(value: inout Value, nextValue: () -> Value) {
        value.merge(nextValue(), uniquingKeysWith: {  })
    }
}

struct Child: View {

    let id: Int
    @State var currentPosition: CGSize = .zero

    // ... other code

    var body: some View {
        Circle()

               // ... other code
        )
        .preference(key: ViewPositionsKey.self, value: [id: currentPosition])
    }
}

struct ParentView: View {
    var body: some View {
        ZStack { // example fo container
           
            // your child views here

        }
        .onPreferenceChange(ViewPositionsKey.self) { value in
            // value is [childId: Position] - do anything needed
        }
    }
}

我们讨论过的一种方法是您可以将数据存储在 EnviornmentObject 中并创建一个 object 来存储它的属性,视图将进行绑定,视图的工作是更新object 的属性。在您的情况下,此视图为 ChildView。因为我从以前的帖子中知道你的代码,所以我将把它包含在这里。

我已将 Child 重命名为 ChildView 因为实际上它的工作只是显示圆圈并更新它,但另外我创建了一个名为 Child 的模型,这就是我们想要呈现。

Child.swift

import SwiftUI

struct Child: Identifiable {
    let id: UUID = UUID()
    var location: CGSize

    init(location: CGSize = .zero) {
        self.location = location
    }
}

这是一个非常简单的声明,我们指定了一个 location 和一个 ID 以便能够识别它。

然后我把ChildView改成了下面的

ChildView.swift

struct ChildView: View {
    @Binding var child: Child
    var onDragged = {}
    
    @State private var isInitalDrag = true
    @State private var isOnce = true
    @State private var currentPosition: CGSize = .zero
    @State private var newPosition: CGSize = .zero
    
    var body: some View {
        Circle()
            .frame(width: 50, height: 50)
            .foregroundColor(.blue)
            .offset(self.currentPosition)
            .gesture(
                DragGesture()
                    .onChanged { value in
                        if self.isInitalDrag && self.isOnce {
                            self.onDragged()
                            self.isOnce = false
                        }
                        
                        self.currentPosition = CGSize(
                            width: CGFloat(value.translation.width + self.newPosition.width),
                            height: CGFloat(value.translation.height + self.newPosition.height)
                        )
                        
                        self.child.location = self.currentPosition
                    }
                    .onEnded { value in
                        self.newPosition = self.currentPosition
                        self.isOnce = true
                        self.isInitalDrag = false
                    }
            )
            .onAppear {
                  // Pay attention whenever the circle view appears we update it's currentPosition and newPosition to be the child's location
                  self.currentPosition = self.child.location
                  self.newPosition = self.child.location
            }
    }
    
    func onDragged(_ callaback: @escaping () -> ()) -> some View {
        ChildView(child: self.$child, onDragged: callaback)
    }
}

如您所见,我删除了一些以前的代码,因为它们无关紧要。目标是每个ChildView给我们呈现1个Childobject;因此,我们的 ChildView 包含一个名为 child 的绑定 属性。我还将我们的其余属性更改为 private,因为实际上没有理由与不同的视图共享这些状态。

另请注意,每当有拖动时,我都会更改 child object 的位置 属性。这非常重要,因为现在每当我们在任何视图中引用此 child 时,它都将具有相同的位置。

此外,请注意,我已从 ChildView 中删除了 @EnvironmentObject,因为它实际上不需要更改我们的 environment,而只是宣布它正在被拖动,无论哪个视图正在调用它可以在拖动时执行不同的操作,也许一个想要创建新的 child 但另一个想要更改颜色。因此,最好将它们分开以实现可伸缩性。将 ChildView 视为一个组件,而不是真正的完整视图。

然后我把我们的EnvironmentObject改成如下

AppState.swift(我想你叫它DataBridge我懒得改名字了:D)

class AppState : ObservableObject {
    @Published var childInstances: [Child] = []
    
    init() {
        self.createNewChild()
    }
    
    func createNewChild() {
        let child = Child()
        self.childInstances.append(child)
    }
}

它比前面的代码简单多了,因为它真的只有一个Child的数组并且非常仔细地注意,它是一个object Child的数组而不是视图ChildView 和以前一样!它还包括一个函数,可以在调用时创建一个新的 child object。

你的ContentView

终于来了

ContentView.swift

struct ContentView: View {
    @EnvironmentObject var appState: AppState
    
    var body: some View {
        ZStack {
            ForEach(self.appState.childInstances.enumerated().map({[=13=]}), id:\.element.id) { index, child in
                ChildView(child: self.$appState.childInstances[index])
                    .onDragged {
                        self.appState.createNewChild()
                    }
            }

            VStack {
                ForEach(self.appState.childInstances, id: \.self.id) { child in
                    Text("y: \(child.location.height) : x: \(child.location.width)")
                }
            }
            .offset(y: -250)
        }
    }
}

在这个文件中,我们所做的就是枚举我们的 child 个实例(同样 object 不是视图),并且对于每个 child 我们正在创建一个新视图和将 child object 作为 Binding 传递给它,因此每当 ChildView 进行更改时,它实际上会更改原始 Child object。另请注意,我在此视图中处理 .onDragged,因为它是控制应用程序的真实视图,而不是描述 object.

的部分组件

如果时间过长,我深表歉意,但我已尽力解释所有内容,以免造成混淆。这是一种可扩展的方法,因为现在您的 Child 可以有多个属性,也许每个 child 可以有自己的随机颜色而不是蓝色?现在可以通过在 Child 模型中创建一个名为 color 的新 属性 然后在 ChildView.

中引用它来实现

此架构现在还允许您在不同的视图中调用它 ChangeColorView.swift 以引用我们 AppState.childInstances 中的任何 child,然后在位置 =一个不同的圆圈位置,然后将它们的颜色设置为相同,等等......真的没有极限。这称为 OOP(Object 面向编程)。

如果我能提供进一步的帮助,请告诉我。