swiftswiftuiswiftui-state

How can I call a function of a child view from the parent view in swiftUI to change a @state variable?


I'm trying to get into swift/swiftui but I'm really struggling with this one:

I have a MainView containing a ChildView. The ChildView has a function update to fetch the data to display from an external source and assign it to a @State data variable.

I'd like to be able to trigger update from MainView in order to update data.

I've experienced that update is in fact called, however, data is reset to the initial value upon this call.

The summary of what I have:

struct ChildView: View {
    
    @State var data: Int = 0

     var body: some View {
       Text("\(data)")

        Button(action: update) {
            Text("update") // works as expected 
        }
     }

    func update() {
        // fetch data from external source 
        data = 42
    }
}
struct MainView: View {
    var child = ChildView()
 
    var body: some View {
        VStack {
           child
           
           Button(action: {
              child.update()
           }) {
              Text("update")  // In fact calls the function, but doesn't set the data variable to the new value
           }
        }
    }
}

When googling for a solution, I only came across people suggesting to move update and data to MainView and then pass a binding of data to ChildView.

However, following this logic I'd have to blow up MainView by adding all the data access logic in there. My point of having ChildView at all is to break up code into smaller chunks and to reuse ChildView including the data access methods in other parent views, too. I just cannot believe there's no way of doing this in SwiftUI.


Solution

  • Is completely understandable to be confused at first with how to deal with state on SwiftUI, but hang on there, you will find your way soon enough.

    What you want to do can be achieved in many different ways, depending on the requirements and limitations of your project. I will mention a few options, but I'm sure there are more, and all of them have pros and cons, but hopefully one can suit your needs.

    Binding

    Probably the easiest would be to use a @Binding, here a good tutorial/explanation of it.

    An example would be to have data declared on your MainView and pass it as a @Binding to your ChildView. When you need to change the data, you change it directly on the MainView and will be reflected on both.

    This solutions leads to having the logic on both parts, probably not ideal, but is up to what you need.

    Also notice how the initialiser for ChildView is directly on the body of MainView now.

    Example

    struct ChildView: View {
        
        @Binding var data: Int
    
         var body: some View {
           Text("\(data)")
    
            Button(action: update) {
                Text("update") // works as expected 
            }
         }
    
        func update() {
            // fetch data from external source 
            data = 42
        }
    }
    
    struct MainView: View {
        @State var data: Int = 0
     
        var body: some View {
            VStack {
               ChildView(data: $data)
               
               Button(action: {
                  data = 42
               }) {
                  Text("update")  // In fact calls the function, but doesn't set the data variable to the new value
               }
            }
        }
    }
    

    ObservableObject

    Another alternative would be to remove state and logic from your views, using an ObservableObject, here an explanation of it.

    Example

    class ViewModel: ObservableObject {
        @Published var data: Int = 0
    
        func update() {
            // fetch data from external source 
            data = 42
        }
    }
    
    struct ChildView: View {
        @ObservedObject var viewModel: ViewModel
    
         var body: some View {
           Text("\(viewModel.data)")
    
            Button(action: viewModel.update) {
                Text("update") // works as expected 
            }
         }
    }
    
    struct MainView: View {
        @StateObject var viewModel = ViewModel()
     
        var body: some View {
            VStack {
               ChildView(viewModel: viewModel)
               
               Button(action: {
                  viewModel.update()
               }) {
                  Text("update")  // In fact calls the function, but doesn't set the data variable to the new value
               }
            }
        }
    }