在SwiftUI中为每个实例使用双向绑定进行实时数据更改 [英] Real-time data changes using two-way binding for each instance in SwiftUI

查看:602
本文介绍了在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))
    }
}

解决方案

我们讨论过的一种方法是,您可以将数据存储在EnviornmentObject中,并创建一个对象来存储其属性,然后视图将进行绑定并视图的工作是更新对象的属性.在您的情况下,此视图为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
    }
}

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

然后我将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个Child对象;因此,我们的ChildView包含一个称为child的绑定属性.我也将其余属性更改为private,因为确实有0个理由可以用不同的视图共享这些状态.

还请注意,只要有拖动,我就会更改child对象的location属性.这非常重要,因为现在无论何时我们在任何视图中引用此子对象,它都将具有相同的位置.

此外,请注意,我已经从ChildView中删除了@EnvironmentObject,因为它实际上不需要更改我们的environment,而只是宣布它正在被拖动,并且无论调用哪个视图,它都可以在执行以下操作时执行不同的操作拖动时,可能一个想要创建新的孩子,而另一个想要更改颜色.因此,最佳实践是将它们分开以实现可伸缩性.认为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数组并且需要非常注意,它是对象Child的数组,而不是以前的视图ChildView!它还包含一个函数,无论何时调用它都会创建一个新的子对象.

最后是您的ContentView

ContentView.swift

struct ContentView: View {
    @EnvironmentObject var appState: AppState
    
    var body: some View {
        ZStack {
            ForEach(self.appState.childInstances.enumerated().map({$0}), 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对象作为Binding传递给它,因此无论何时ChildView进行更改,实际上将更改原始的Child对象.还要注意,我在此视图中处理.onDragged,因为它是控制应用程序的真实视图,而不是描述对象的部分组件.

很长很抱歉,但是我试图解释所有事情,以免引起混淆.这是一种可扩展的方法,因为现在您的Child可以具有多个属性,也许每个孩子都可以拥有自己的随机颜色而不是蓝色?现在可以通过在Child模型中创建一个名为color的新属性,然后在ChildView中引用它来实现.

此架构现在还允许您在不同的视图中调用它,例如ChangeColorView.swift以引用我们的AppState.childInstances中的任何子对象,然后在location =不同圆的位置时更改其颜色,然后将其颜色设置为一样,等等...真的是天空才是极限.这就是所谓的OOP(面向对象编程).

让我知道我是否可以提供进一步的帮助.

I would like to create instances of Circle() nodes where the user can tap and drag them around on the screen. Each time a Circle() node is pulled from its starting position a new one is created in its place, allowing the user to create as many as they want.

I then want to have this real-time changing data of the screen position for each of the instances created, yet in different views of my choosing, so I may use it for further graphics and effects.

How can I access each individual instance’s real-time screen position data from a different view?

Here is the child view I want to create instances of, accessing the currentPosition variable:

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

解决方案

One approach we had conversation about is you can store your data in an EnviornmentObject and create an object to store it's properties and the view will take a binding and the view's job is to update the object's properties. In your case this view is ChildView. So because I know your code from previous posts I will include it here.

I have renamed Child to ChildView because really it's job is just to show the circle and update it, but additionally I have created a model called Child which is what we want to present.

Child.swift

import SwiftUI

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

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

It's a very simple declaration, we have specified a location and an ID to be able to identify it.

then I changed ChildView to the following

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

So as you can see I have removed some of the previous code as it will be irrelevant. The goal is that each ChildView will present 1 Child object for us; hence, our ChildView includes a binding property called child. I have also changed the rest of our properties to be private as there is really 0 reason to share these states with different views.

Also notice whenever there is a drag I change the child object's location property. This is very important because now whenever we reference this child in any view it will have the same location.

Additionally, note I have removed @EnvironmentObject from the ChildView as it really doesn't need to change our environment instead it only announces that it is being dragged and whichever view is calling it can do different actions when dragging, maybe one wants to create new child but the other wants to change a color. So it's best practice to separate these for scalability. think of ChildView as a component than a real full blown view.

I then changed our EnvironmentObject to be as follows

AppState.swift (I think you called it DataBridge I was lazy to change the name :D)

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

It's much simpler than the previous code, as it really has only an array of Child and pay very close attention, its an array of the object Child not the view ChildView like you had before! it also includes a function to create a new child object whenever it's called.

Finally here is your ContentView

ContentView.swift

struct ContentView: View {
    @EnvironmentObject var appState: AppState
    
    var body: some View {
        ZStack {
            ForEach(self.appState.childInstances.enumerated().map({$0}), 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)
        }
    }
}

In this file all we are doing is enumerating through our child instances (again the objects not the views) and for each child we are creating a new view and passing it the child object as a Binding so whenever the ChildView makes changes it will actually change the original Child object. Also note that I handle .onDragged in this view since it's a real view that controls the app and not a partial component that describes an object.

I apologize if it's long but I tried to explain everything so it won't be confusing. This is a scalable approach because now your Child can have multiple properties, maybe each child can have it's own random color instead of blue? that can be now possible by creating a new property in Child model called color and then you reference it in your ChildView.

This architecture now also allows you to for example in a different view lets call it ChangeColorView.swift to reference any child from our AppState.childInstances and then change its color when the location = a different circle's location then set their colors to be the same, etc.... really the sky is the limit. This is known as OOP (Object Oriented Programming).

Let me know if I can help any further.

这篇关于在SwiftUI中为每个实例使用双向绑定进行实时数据更改的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆