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.
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.
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.
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
}
}
}
}
Another alternative would be to remove state and logic from your views, using an ObservableObject
, here an explanation of it.
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
}
}
}
}