Josh
Josh

Reputation: 1718

SwiftUI send button tap to subview

I have a few views that contain the same button with some different content. Because of this I made a ContainerView that houses the shared Button layout, and has room for a generic ContentView.

I want the ContentView to respond when the ContainerView button is tapped.

Using UIKit, I would hold a reference to the ContentView in the ContainerView and call a function on it when the button was hit. However, because SwiftUI has all the views as structs, the contentView is copied when put into the ContainerView's body. Thus the reference and the shown ContentView are different & I cannot send the subview a message.

Code:

struct ContainerView: View {
    let contentView = ContentView()

    var body: some View {
        Group {
            /// When this button is tapped, I would like to send a message to the `ContentView`.
            Button(action: self.reset, label: { Text("RESET") })
            /// Unfortunately, this seemes to send a copy of the `contentView`. So I am unable to send the
            /// corrent struct a message.
            ///
            /// How can I send a subview a message from the superview?
            self.contentView
        }
    }

    func reset() {
        self.contentView.reset()
    }
}

struct ContentView: View {
    @State private var count: Int = 0

    var body: some View {
        Group {
            Text("Count: \(self.count)")
            Button(action: self.increment, label: { Text("Increment") })
        }
    }

    func increment() {
        self.count += 1
    }

    /// When this is called from the `ContainerView`, it is accessing a different ContentView 
    /// struct than is being displayed.
    func reset() {
        self.count = 0
    }
}

So the question is: how can I send a message to & run some code in the ContentView when a button in the ContainerView is tapped?

Upvotes: 2

Views: 3312

Answers (1)

Josh Hrach
Josh Hrach

Reputation: 1376

Instead of trying to store a reference to the subview, why not make a binding between them? In your example, this could be by binding to the count.

struct ContainerView: View {
    @State private var count = 0

    var body: some View {
        // Your Button wrapping the ContentView
        ContentView(count: $count)
    }

    func reset() {
        self.count = 0
    }
}


struct ContentView: View {
    @Binding var count: Int

    // ContentView's body
}

When the ContainerView resets the count, the binding will update the child.

EDIT: I see your comments about wanting ContentView to control the reset logic. What about trying to replicate some of the functionality of things like NavigationLink, where an isActive: bool is set, then reset, by the system on navigation?

In your case, you could try the following:

struct ContainerView: View {
    @State private var shouldReset: Bool = false

    var body: some View {
        // Your Button wrapping the ContentView
        ContentView(shouldReset: $shouldReset)
    }

    func reset() {
        self.shouldReset = true
    }
}


struct ContentView: View {
    @Binding var shouldReset: Bool {
        didSet {
            if shouldReset {
                // Call your reset logic here
            }
            shouldReset = false
        }
    }

    // ContentView's body
}

Your ContentView will know the change, we'll see it as a separate "state", and then that state is reset once the action is complete.

It's probably not the ideal solution, but to me it seems to replicate a pattern shown by some first party SwiftUI components.

Upvotes: 6

Related Questions