你如何从 SwiftUI 的子视图调用函数?

How do you call a function from a subview in SwiftUI?

我有一个 child/subview,它是父视图中实例的模板。这个子视图是一个圆圈,用户可以在屏幕上移动。这个圆圈的初始拖动应该调用父视图中的函数,该函数附加子视图的新实例。基本上,最初将圆从其起始位置移动会在该起始位置创建一个新圆。然后,当您最初移动刚刚创建的新圆时,会​​在该起始位置再次创建另一个新圆,依此类推...

如何从子视图调用 ContentView 中的 addChild() 函数?

谢谢,感谢您的帮助。

import SwiftUI

struct ContentView: View {
    
    @State var childInstances: [Child] = [Child(stateBinding: .constant(.zero))]
    @State var childInstanceData: [CGSize] = [.zero]
    @State var childIndex = 0
    func addChild() {
        self.childInstanceData.append(.zero)
        
        self.childInstances.append(Child(stateBinding: $childInstanceData[childIndex]))
        
        self.childIndex += 1
    }
    
    var body: some View {
        ZStack {
            ForEach(childInstances.indices, id: \.self) { index in
                self.childInstances[index]
            }
            
            ForEach(childInstanceData.indices, id: \.self) { index in
                Text("y: \(self.childInstanceData[index].height) : x: \(self.childInstanceData[index].width)")
                    .offset(y: CGFloat((index * 20) - 300))
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
import SwiftUI

struct Child: View {
    @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... How do I do it?
                            ContentView().addChild()
                            
                            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))
    }
}

这就是我要做的,我会在第二个视图中创建一个函数并在任何情况下调用它,比如当第二个视图中的按钮被按下时调用这个函数。我会在主视图中传递函数的回调。

这里有一段代码可以证明我的意思。

import SwiftUI

struct Whosebug23: View {
    var body: some View {
        VStack {
            Text("First View")
            
            Divider()
            
            // Note I am presenting my second view here and calling its function ".onAdd"
            SecondView()
                // Whenever this function is invoked inside `SecondView` then it will run the code in between these brackets.
                .onAdd {
                    print("Run any function you want here")
                }
        }
    }
}

struct Whosebug23_Previews: PreviewProvider {
    static var previews: some View {
        Whosebug23()
    }
}


struct SecondView: View {
    // Define a variable and give it default value
    var onAdd = {}
    
    var body: some View {
        Button(action: {
            // This button will invoke the callback stored in the variable, this can be invoked really from any other function. For example, onDrag call self.onAdd (its really up to you when to call this).
            self.onAdd()
        }) {
            Text("Add from a second view")
        }
    }
    
    
    // Create a function with the same name to keep things clean which returns a view (Read note 1 as why it returns view)
    // It takes one argument which is a callback that will come from the main view and it will pass it down to the SecondView
    func onAdd(_ callback: @escaping () -> ()) -> some View { 
        SecondView(onAdd: callback)
    }
}

注意 1:我们的 onAdd 函数 return 是视图的原因是因为请记住 swiftui 仅基于视图,每个修饰符 return 本身都是视图。例如,当您有一个 Text("test") 然后向其添加 .foregroundColor(Color.white) 时,实际上您所做的是您没有修改文本的颜色,而是创建了一个新的 Text使用自定义 foregroundColor 值。

这正是我们正在做的,我们正在创建一个可以在调用 SecondView 时初始化的变量,但我们没有在初始化程序中调用它,而是将其创建为函数修饰符,它将 return SecondView 的新实例,我们的变量具有自定义值。

我希望这是有道理的。如果您有任何问题,请随时提问。

这是您修改的代码:

ContentView.swift


import SwiftUI

struct ContentView: View {
    
    @State var childInstances: [Child] = [Child(stateBinding: .constant(.zero))]
    @State var childInstanceData: [CGSize] = [.zero]
    @State var childIndex = 0
    func addChild() {
        self.childInstanceData.append(.zero)
        
        self.childInstances.append(Child(stateBinding: $childInstanceData[childIndex]))
        
        self.childIndex += 1
    }
    
    var body: some View {
        ZStack {
            ForEach(childInstances.indices, id: \.self) { index in
                self.childInstances[index]
                    .onAddChild {
                        self.addChild()
                    }
            }
            
            ForEach(childInstanceData.indices, id: \.self) { index in
                Text("y: \(self.childInstanceData[index].height) : x: \(self.childInstanceData[index].width)")
                    .offset(y: CGFloat((index * 20) - 300))
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Child.swift

import SwiftUI

struct Child: View {
    @Binding var stateBinding: CGSize
    
    @State var isInitalDrag = true
    @State var isOnce = true
    
    @State var currentPosition: CGSize = .zero
    @State var newPosition: CGSize = .zero
    
    var onAddChild = {} // <- The variable to hold our callback
    
    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... How do I do it?
                            
                            self.onAddChild() <- // Here is your solution
                            
                            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
                    }
            )
    }
    
// Our function which will initialize our variable to store the callback
    func onAddChild(_ callaback: @escaping () -> ()) -> some View {
        Child(stateBinding: self.$stateBinding, isInitalDrag: self.isInitalDrag, isOnce: self.isOnce, currentPosition: self.currentPosition, newPosition: self.newPosition, onAddChild: callaback)
    }
}

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

这是你在我的代码中的答案:

import SwiftUI

struct ContentView: View {
    @State var childInstances: [Child] = []
    @State var childInstanceData: [CGSize] = []
    @State var childIndex = 0
    func addChild() {
        self.childInstanceData.append(.zero)
        
        self.childInstances.append(Child(stateBinding: $childInstanceData[childIndex]))
        
        self.childIndex += 1
    }
    
    var body: some View {
        ZStack {
            ForEach(childInstances.indices , id: \.self) { index in
                self.childInstances[index]
                    .onAddChild {
                        print(self.childInstances.count)
                        self.addChild()
                    }
            }
            VStack {
                ForEach(childInstanceData.indices, id: \.self) { index in
                    Text("\(index).  y: \(self.childInstanceData[index].height) : x: \(self.childInstanceData[index].width)")
                }
            }
            .offset(y: -250)
            
        }
        .onAppear {
            self.addChild()
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
import SwiftUI

struct Child: View {
    @Binding var stateBinding: CGSize

    var onAddChild = {} // <- The variable to hold our callback
    
    @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.onAddChild()
                            
                            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
                    }
            )
    }
    
    func onAddChild(_ callback: @escaping () -> ()) -> some View {
        Child(stateBinding: self.$stateBinding, onAddChild: callback)
    }
}

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