Siddharth Kamaria
Siddharth Kamaria

Reputation: 2707

SwiftUI - Should you use `@State var` or `let` in child view when using ForEach

I think I've a gap in understanding what exactly @State means, especially when it comes to displaying contents from a ForEach loop.

My scenario: I've created minimum reproducible example. Below is a parent view with a ForEach loop. Each child view has aNavigationLink.

// Parent code which passes a Course instance down to the child view - i.e. CourseView
struct ContentView: View {
    
    @StateObject private var viewModel: ViewModel = .init()
    
    var body: some View {
        NavigationView {
            VStack {
                ForEach(viewModel.courses) { course in
                    NavigationLink(course.name + " by " + course.instructor) {
                        CourseView(course: course, viewModel: viewModel)
                    }
                }
            }
        }
    }
}

class ViewModel: ObservableObject {
    @Published var courses: [Course] = [
        Course(name: "CS101", instructor: "John"),
        Course(name: "NS404", instructor: "Daisy")
    ]
}

struct Course: Identifiable {
    var id: String = UUID().uuidString
    var name: String
    var instructor: String
}

Actual Dilemma: I've tried two variations for the CourseView, one with let constant and another with a @State var for the course field. Additional comments in the code below.

The one with the let constant successfully updates the child view when the navigation link is open. However, the one with @State var doesn't update the view.

struct CourseView: View {
    
    // Case 1: Using let constant (works as expected)
    let course: Course

    // Case 2: Using @State var (doesn't update the UI)
    // @State var course: Course

    @ObservedObject var viewModel: ViewModel
    
    var body: some View {
        VStack {
            Text("\(course.name) by \(course.instructor)")
            Button("Edit Instructor", action: editInstructor)
        }
    }
    
    // Case 1: It works and UI gets updated
    // Case 2: Doesn't work as is. 
    //    I've to directly update the @State var instead of updating the clone -
    //    which sometimes doesn't update the var in my actual project 
    //    (that I'm trying to reproduce). It definitely works here though.
    private func editInstructor() {
        let instructor = course.instructor == "Bob" ? "John" : "Bob"
        var course = course
        course.instructor = instructor
        save(course)
    }
    
    // Simulating a database save, akin to something like GRDB
    // Here, I'm just updating the array to see if ForEach picks up the changes
    private func save(_ courseToSave: Course) {
        guard let index = viewModel.courses.firstIndex(where: { $0.id == course.id }) else {
            return
        }
        viewModel.courses[index] = courseToSave
    }
}

What I'm looking for is the best practice for a scenario where looping through an array of models is required and the model is updated in DB from within the child view.

Upvotes: 2

Views: 882

Answers (1)

swiftPunk
swiftPunk

Reputation: 1

Here is a right way for you, do not forget that we do not need put logic in View! the view should be dummy as possible!

struct ContentView: View {
    
    @StateObject private var viewModel: ViewModel = ViewModel.shared
    
    var body: some View {
        NavigationView {
            VStack {
                ForEach(viewModel.courses) { course in

                    NavigationLink(course.name + " by " + course.instructor, destination: CourseView(course: course, viewModel: viewModel))

                }
            }
        }
    }
}




struct CourseView: View {

    let course: Course

    @ObservedObject var viewModel: ViewModel
    
    var body: some View {
        VStack {
            Text("\(course.name) by \(course.instructor)")
            Button("Update Instructor", action: { viewModel.update(course) })
        }
    }
    

}


class ViewModel: ObservableObject {
    static let shared: ViewModel = ViewModel()
    @Published var courses: [Course] = [
        Course(name: "CS101", instructor: "John"),
        Course(name: "NS404", instructor: "Daisy")
    ]
    

    func update(_ course: Course) {
        guard let index = courses.firstIndex(where: { $0.id == course.id }) else {
            return
        }

        courses[index] = Course(name: course.name, instructor: (course.instructor == "Bob") ? "John" : "Bob")
    }
    
    
}

struct Course: Identifiable {
    let id: String = UUID().uuidString
    var name: String
    var instructor: String
}

Upvotes: 1

Related Questions