I have a list of users in a navigation stack, each item wrapped by a navigation link.
struct UserListView: View {
@ObservedObject private var viewModel = ViewModel()
var body: some View {
NavigationStack {
List {
ForEach(viewModel.users) { user in
NavigationLink(value: user.id) {
Text(user.givenName)
}
}
}
.navigationTitle("UserList")
.navigationDestination(for: Int.self, destination: { id in
FormView(id: id) // gets called multiple times, 2 times in my experience
})
.onAppear {
viewModel.findAll()
}
}
}
}
The problem is in the FormView that gets init multiple times, and as a result, the same for it's associated viewModel, and as I call fetch department list from the network layer, by the time the result comes back, the caller (viewModel) is already gone (deinit).
struct UserFormView: View {
@ObservedObject private var viewModel = FormViewModel()
@State var first: String = ""
@State var last: String = ""
@State var department: Department? = nil
var id: Int?
var body: some View {
VStack {
HStack {
TextField("First", text: $first)
.textFieldStyle(RoundedBorderTextFieldStyle())
.font(.system(size: 16))
TextField("Last", text: $last)
.textFieldStyle(RoundedBorderTextFieldStyle())
.font(.system(size: 16))
}
HStack {
Picker(selection: $department, label: Text("")) {
ForEach(viewModel.departments, id: \.self) { department in
Text(department.name ?? "").tag(department)
}
}
.pickerStyle(WheelPickerStyle())
.padding()
}
}
.padding()
.navigationTitle("User Form")
.onAppear {
viewModel.findAllDepartments()
}
}
}
Here's my ObservableObject that gets no chance to update the departments, because this object gets deinit before that.
I could tell from the print statement in the init and deinit as they come and go multiple times.
class FormViewModel: ObservableObject {
@Published var user: User? = nil
@Published var departments: [Department] = []
private var cancellable = Set<AnyCancellable>()
private var rand = Int(arc4random_uniform(10)) + 1
init() {
print("init \(rand)") // gets called multiple times
}
func findAllDepartments() {
delegate?.findAllDepartments()
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: {[weak self] completion in
switch completion{
case .finished:
print("finished")
case .failure(let error):
print(error)
}
}, receiveValue: { [weak self] departments in
self?.departments.append(contentsOf: departments) // no chance, it's deallocated already
self?.objectWillChange.send()
})
.store(in: &cancellable)
}
deinit {
print("deinit \(rand)") // multiple times
}
}
Question: I understand it's perhaps the nature of the SwiftUI to create and re-recreate Views as state changes, but how can we be consistent then?
@ObservedObjectwhere you own the object. It should be@StateObject.@StateObjectprevents the object from getting recreated on each render.ObservedObjectfrom@ObservedObject private var viewModel = FormViewModel(), ? and use@@StateObject private var viewModel = FormViewModel()@ObservedObjectwith@StateObject