How do you call a function from a subview in SwiftUI?
Asked Answered
H

2

6

I have a child/subview which is a template for instances inside the parent view. This child view a is circle that can be moved around the screen by the user. The initial drag of this circle is supposed to call a function in the parent view, which appends a new instance of the child view. Basically, initially moving the circle from its starting position creates a new circle in that start position. Then when you initially move that new one, which was just created, another new circle is created at that starting position again, etc...

How do I call the addChild() function that's in ContentView from the Child view?

Thank you, I appreciate any help.

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))
    }
}
Halle answered 17/7, 2020 at 20:10 Comment(1)
Does this answer your question? SwiftUI - Button - How to pass a function request to parentOctant
B
1

Here is what I would do, I would create a function in the second view and invoke it under any circumstance, say when a button in the second view is pressed then invoke this function. And I would pass the function's callback in the main view.

Here is a code to demonstrate what I mean.

import SwiftUI

struct StackOverflow23: 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 StackOverflow23_Previews: PreviewProvider {
    static var previews: some View {
        StackOverflow23()
    }
}


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

NOTE 1: The reason our onAdd function returns a view is because remember that swiftui is based on only views and every modifier returns a view itself. For example when you have a Text("test") and then add .foregroundColor(Color.white) to it, what essentially you are doing is that you are not modifying the color of the text but instead you are creating a new Text with a custom foregroundColor value.

And this is exactly what we are doing, we are creating a variable that can be initialized when calling SecondView but instead of calling it in the initializer we created it as a function modifier that will return a new instance of SecondView with a custom value to our variable.

I hope this makes sense. If you have any questions don't hesitate to ask.

And here is your code modified:

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))
    }
}
Borden answered 17/7, 2020 at 22:11 Comment(7)
I am glad I was able to help! if you have further questions don't hesitate to ask. Also consider marking the answer so others can easily find help if they have a similar concern.Borden
Wow, Muhand you're amazing! Thank you so much!!! It totally works now! I've been beating my head over this problem for almost two months now... I Have a general understanding. I get it the way you explain, yet the complexities to it, such as the @escaping parameter and it being a function modifier, are some things I need to explore more. For the most part I see what you're saying about recreating the second view with the callback. Kudos! ~RichHalle
I myself not a swift developer I am mainly a backend engineer but swift is very simple and I think with practice you will get the hang of it in no time. SwiftUI is built on top of Swift so it doesn't hurt to learn Swift and Closures in swift. Good luck!Borden
Hey @Muhand, I do have another question. To call the function in the parent view from the child view was just one direction to go. The other would be to create a class that inherits from ObservableObject and is the hub for @Published global variables. When I tried that I had no trouble calling functions from any view, yet I couldn't create a two way binding from within the class. How can I accomplish that? The $ doesn't work in classes because it only works with @State... self.childInstances.append(Child(stateBinding: $childInstanceData[childIndex]))Halle
@Halle can you please make a new question and I will answer it there, as it is a little bit different from this question and we don't want to confuse people who have a similar question as you. Additionally, the added answer will just be too long. But in short it is doable using EnvironmentObject, In fact I have converted your code to use EnvironmentObject instead.Borden
Yes, I just made a new question here: (#63004683) Should I delete these last couple of comments to keep it clean?Halle
No its fine you dont need to delete them. I will post my answer as soon as I am done with what I am doing. Shouldn't take long!Borden
H
0

Here's your answer within my code:

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))
    }
}
Halle answered 20/7, 2020 at 4:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.