Swiftui Pass Two Child Views to View

SwiftUI Pass two child views to View

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

struct FlippableView<V1: View, V2: View>: View {
@State private var flipped = false
@State private var degrees = 0.0

var frontCard: V1
var backCard: V2

@inlinable public init(@ViewBuilder content: () -> TupleView<(V1, V2)>) {
let t = content()
self.frontCard = t.value.0
self.backCard = t.value.1
}

var body: some View {
return Group() {
if self.degrees < 90 {
self.frontCard
} else {
self.backCard
.rotation3DEffect(Angle(degrees: 180), axis: (x: CGFloat(0), y: CGFloat(10), z: CGFloat(0)))

}
}
}
}

Sharing a @State var Bool between 2 child Views in SwiftUI

I have spent hours on Internet to learn more about using @State/@Binding or @EnvironmentObject

So you are on the right path because you do need to use @Binding. Maybe you just had the wrong idea of how to connect the dots.

Basically you need the @State property wrapper in your parent or ContentView and since you want to share that same property in your child views and react to any mutation on it, you use the @Binding property wrapper in the child views.

So you would have something like this:

struct ContentView: View {
@State var showHello = true

var body: some View {
VStack {
TopView(showHello: $showHello)
BottomView(showHello: showHello)
}
}
}

struct TopView: View {
@Binding var showHello: Bool

var body: some View {
Button("Show Hello", action: { showHello.toggle() })
.padding()
}
}

struct BottomView: View {
let showHello: Bool

var body: some View {
Text("This is your message:")
Text("Hello")
.opacity(showHello ? 1 : 0)
}

}

I would recommend some other good resources for SwiftUI:

  • SwiftUI State Management
  • @Binding
  • SwiftUI Articles by John Sundell

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 access to the children views in SwiftUI?

This is what you're looking for, if I'm understanding your question correctly:

struct FullButton<Label>: View where Label: View {
var action: () -> Void
var label: () -> Label

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

This would allow you to pass whatever content you want to be displayed on your button, meaning that the code you have here would now work:

FullButton(action: {
print("touched")
}) {
Text("Button")
}

Update

After looking over your question several times, I've realized that your confusion is stemming from a misunderstanding of what is happening when you create a normal Button.

In the code below, I'm creating a Button. The button takes two arguments - action and label.

Button(action: {}, label: {
Text("Button")
})

If we look at the documentation for Button, we see that it is declared like this:

struct Button<Label> where Label : View

If we then look at the initializers, we see this:

init(action: @escaping () -> Void, @ViewBuilder label: () -> Label)

Both action and label expect closures. action expects a closure with a return type of Void, and label expects a @ViewBuilder closure with a return type of Label. As defined in the declaration for Button, Label is a generic representing a View, so really, label is expecting a closure that returns a View.

This is not unique to Button. Take HStack, for example:

struct HStack<Content> where Content : View

init(alignment: VerticalAlignment = .center, spacing: Length? = nil, @ViewBuilder content: () -> Content)

Content serves the same purpose here that Label does in Button.

Something else to note - when we create a button like this...

Button(action: {}) {
Text("Button")
}

...we're actually doing the same thing as this:

Button(action: {}, label: {
Text("Button")
})

In Swift, when the last argument in a method call is a closure, we can omit the argument label and append the closure to the outside of the closing parenthesis.

In SwiftUI, you cannot implicitly pass content to any View. The View must explicitly accept a @ViewBuilder closure in its initializer.

And so, you cannot pass a @ViewBuilder closure to FullButton unless FullButton accepts a @ViewBuilder closure as an argument in its initializer, as shown at the beginning of my answer.

What is the correct way to share state variables between parent and child views in SwiftUI

When you want to share mutable state across views you probably want to take advantage of a class conforming to ObservableObject. You might end up with something a little more like this:

class CustomState: ObservableObject {
@Published var options: [String]
@Published var selected:[Bool] = []

init(options: [String]) {
self.options = options
}
}

struct CustomParentView: View {
@ObservedObject var a = CustomState(options: ["a","b","c"])
@ObservedObject var b = CustomState(options: ["d","e","f"])

var body: some View {
ScrollView {
VStack{
CustomChildView(state: a)
CustomChildView(state: b)
}
}
}
}

struct CustomChildView: View {
@ObservedObject var state: CustomState

// ...

func selectButtonPressed(_ x:Int) {
for i in 0..<state.options.count {
state.selected[i] = (i == x)
}
}
}

Accessing @State variables in child views from parent views SwiftUI

You can declare your child view properties as @Binding instead of @State:

struct DetailedView: View {
...
@State public var exercise: String = "Bench Press"
}

struct ExerciseCard: View {
@Binding public var exercise: String
...
}

and then pass the variable from the parent view to the child view:

ExerciseCard(exercise: $exercise)


Related Topics



Leave a reply



Submit