Swiftui - Button - How to Pass a Function (With Parameters) Request to Parent from Child

SwiftUI - Button - How to pass a function request to parent

The best examples on closures and how they can be used is found in the official swift documentation.

This is a small example on how to pass a closure to your child view which then calls a function of the parent:

struct ChildView: View {
var function: () -> Void

var body: some View {
Button(action: {
self.function()
}, label: {
Text("Button")
})
}
}

struct ParentView: View {
var body: some View {
ChildView(function: { self.fuctionCalledInPassedClosure() })
}

func fuctionCalledInPassedClosure() {
print("I am the parent")
}
}

I hope this helps!

Pass a function

And here is an example to pass the function:

struct ChildView: View {
var function: () -> Void

var body: some View {
Button(action: {
self.function()
}, label: {
Text("Button")
})
}
}

struct ParentView: View {
var body: some View {
ChildView(function: self.passedFunction)
}

func passedFunction() {
print("I am the parent")
}
}

Pass a function with parameters

struct ChildView: View {
var myFunctionWithParameters: (String, Int) -> Void

var body: some View {
Button(action: {
self.myFunctionWithParameters("parameter", 1)
}, label: {
Text("Button")
})
}
}

struct ParentView: View {
var body: some View {
ChildView(myFunctionWithParameters: self.passedFunction)
}

func passedFunction(myFirstParameter: String, mySecondParameter: Int) {
print("I am the parent")
}
}

Call parent view's function from child view with parameters

Here is fixed variant. Tested with Xcode 11.4 / iOS 13.4

struct ChildView: View {
var onUpdate: (Double) -> () // << no labels, just types !!

var body: some View {
VStack {
Text("child view")
Button(action: {
self.onUpdate(3.0)
}) {
Text("onUpdate")
}
}
}
}

SwiftUI How to pass data from child to parent as done in C# with the 'Delegate-EventHandler-EventArgs' way

Here is possible solution. Tested with Xcode 11.4 / iOS 13.4

struct ChildView: View {
var function: (String) -> Void

@State private var value = "Child Value"
var body: some View {
Button(action: {
self.function(self.value)
}, label: {
Text("Button")
})
}
}

struct ContentView: View {
var body: some View {
ChildView { self.setViewBackToNil(myStringParameter: $0) }
}

func setViewBackToNil(myStringParameter: String) {
print("I am the parent: \(myStringParameter)")
}
}

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

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

How to send an event from a parent view to a child view in SwiftUI

you could try this broad approach, where the @StateObject var vm = ChildVM() is
declared at the top level (the one source of truth), and the ChildViews all observe
the changes using the passed in ChildVM, through an environmentObject.

struct ParentView : View {
@StateObject var vm = ChildVM() // <-- here at top level

var body : some View {
VStack {
Button( action: { vm.refetchData() }) { // <-- here, all ChildViews will know about it
ChildView1()
ChildView2()
ChildView3()
}
}.environmentObject(vm) // <-- here, will pass it to all ChildViews
}

}

// similarly for ChildView2, ChildView3
struct ChildView1 : View {
@EnvironmentObject var vm: ChildVM // <-- here

var body: some View {
Text("ChildView1")
}
}

class ChildVM: ObservableObject {
init() {
// fetch data
}
func refetchData() {
// refetches data
}

}

See also: https://developer.apple.com/documentation/swiftui/stateobject

Passing a state variable to parent view

In general, your state should always be owned higher up the view hierarchy. Trying to access the child state from a parent is an anti-pattern.

One option is to use @Bindings to pass the values down to child views:

struct BookView: View {

@Binding var title : String
@Binding var author : String

var body: some View {
TextField("Title", text: $title)
TextField("Author", text: $author)
}
}

struct ContentView: View {
@State private var presentNewBook: Bool = false

@State private var title = ""
@State private var author = ""

var body: some View {
NavigationView {
VStack {
Text("Title: \(title)")
Text("Author: \(author)")
Button("Open") {
presentNewBook = true
}
}
}.sheet(isPresented: $presentNewBook) {
BookView(title: $title, author: $author)
}
}
}

Another possibility is using an ObservableObject:

class BookState : ObservableObject {
@Published var title = ""
@Published var author = ""
}

struct BookView: View {

@ObservedObject var bookState : BookState

var body: some View {
TextField("Title", text: $bookState.title)
TextField("Author", text: $bookState.author)
}
}

struct ContentView: View {
@State private var presentNewBook: Bool = false
@StateObject private var bookState = BookState()

var body: some View {
NavigationView {
VStack {
Text("Title: \(bookState.title)")
Text("Author: \(bookState.author)")
Button("Open") {
presentNewBook = true
}
}
}.sheet(isPresented: $presentNewBook) {
BookView(bookState: bookState)
}
}
}

I've altered your example views a bit because to me the structure was unclear, but the concept of owning the state at the parent level is the important element.

How do I pass data from a child view to a parent view to another child view in SwiftUI?

You can use EnvironmentObject for stuff like this...

The nice thing about EnvironmentObject is that whenever and wherever you change one of it's variables, it will always make sure that every where that variable is used, it's updated.

Note: To get every variable to update, you have to make sure that you pass through the BindableObject class / EnvironmentObject on everyView you wish to have it updated in...

SourceRectBindings:

class SourceRectBindings: BindableObject {

/* PROTOCOL PROPERTIES */
var willChange = PassthroughSubject<Application, Never>()

/* Seems Like you missed the 'self' (send(self)) */
var sourceRect: CGRect = .zero { willSet { willChange.send(self) }
}

Parent:

struct ParentView: view {
@EnvironmentObject var sourceRectBindings: SourceRectBindings

var body: some View {
childViewA()
.environmentObject(sourceRectBindings)
childViewB()
.environmentObject(sourceRectBindings)
}
}

ChildViewA:

struct ChildViewA: view {
@EnvironmentObject var sourceRectBindings: SourceRectBindings

var body: some View {

// Update your frame and the environment...
// ...will update the variable everywhere it was used

self.sourceRectBindings.sourceRect = CGRect(x: 0, y: 0, width: 300, height: 400)
return Text("From ChildViewA : \(self.sourceRectBindings.sourceRect)")
}
}

ChildViewB:

struct ChildViewB: view {
@EnvironmentObject var sourceRectBindings: SourceRectBindings

var body: some View {
// This variable will always be up to date
return Text("From ChildViewB : \(self.sourceRectBindings.sourceRect)")
}
}

LAST STEPS TO MAKE IT WORK

• Go into your highest view you want the variable to update which is your parent view but i usually prefer my SceneDelegate ... so this is how i usually do it:

let sourceRectBindings = SourceRectBindings()

• Then in the same class 'SceneDelegate' is go to where i specify my root view and pass through my EnviromentObject

window.rootViewController = UIHostingController(
rootView: ContentView()
.environmentObject(sourceRectBindings)
)

• Then for the last step, I pass it through to may Parent view

struct ContentView : View {

@EnvironmentObject var sourceRectBindings: SourceRectBindings

var body: some View {
ParentView()
.environmentObject(sourceRectBindings)
}
}

If you run the code just like this, you'll see that your preview throws errors. Thats because it didn't get passed the environment variable.
To do so just start a dummy like instance of your environment:

#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(SourceRectBindings())
}
}
#endif

Now you can use that variable how ever you like and if you want to pass anything else other that the rect ... you can just add the variable to the EnvironmentObject class and you can access and update it

How to get variable value of child view in parent view on SwiftUI?

As mentioned in the comments, state should be owned by a parent view and then passed to the child views.

In your case, you don't actually need to use a custom initializer for CancelRow in the example you gave (you could rely on just the synthesized initializer), but as request, this uses an initializer.

struct CancelRow: View {
@Binding var isSelected: Bool

init(isSelected: Binding<Bool>) {
self._isSelected = isSelected
}

@ViewBuilder var body: some View {
HStack {
Text(verbatim: "Hello, \(isSelected)")
Button("Toggle") {
isSelected.toggle()
}
}
}
}

struct SubmitButtonView : View {
var buttonTitle : String
var buttonCallBack : () -> Void
var isActive : Bool

var body: some View {
Text("Submit: \(isActive ? "true" : "false")")
}
}

struct ContentView: View {
@State private var isSelected = false

var title : String {
"Title"
}

var body: some View {
VStack {
CancelRow(isSelected: $isSelected)
SubmitButtonView(buttonTitle: title, buttonCallBack: {
goToOtherScreen()
}, isActive: isSelected )
}
}

func goToOtherScreen() {
print("Navigate")
}
}


Related Topics



Leave a reply



Submit