Reputation: 12571
in the code provided below I am having an issue that the DetailViewModel
is being recreated. That happens because the ContentView
updates, which also recreates all the NavigationLink
s and destinations. Because of this the state within the DetailViewModel
is reset.
This is some example code:
import SwiftUI
import Combine
struct ContentView: View {
let items = ["Item A", "Item B", "Item C"]
@State var contentViewUpdater = 0
var body: some View {
NavigationView {
VStack {
Button("Update ContentView: \(contentViewUpdater)") {
self.contentViewUpdater += 1
}
List(items, id: \.self) { item in
// How to prevent DetailViewModel from recreating after this ContentView receives an update?
NavigationLink(destination: DetailView(model: DetailViewModel(item: item))) {
Text(item)
}
}
}
}
}
}
final class DetailViewModel: ObservableObject {
let item: String
@Published var counter = 0
init(item: String) {
self.item = item
}
}
struct DetailView: View {
@ObservedObject var model: DetailViewModel
var body: some View {
VStack {
Text("Counter for \(model.item): \(model.counter)")
Button("Increase counter") {
self.model.counter += 1
}
}
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
Here is a screenrecording of the issue. The DetailViewModel.counter
var resets if ContentView
updates.
How can you prevent the state in DetailViewModel
from resetting when the parent view updates?
Upvotes: 6
Views: 1981
Reputation: 3809
Apologies, my code is adapted from yours as I've not updated to the latest beta yet, but this works for me. I've used the concept of "Lifting State Up" from React, and moved the model data into the Master view itself.
From a playground:
import SwiftUI
import PlaygroundSupport
final class ItemViewModel : BindableObject {
let willChange = PassthroughSubject<Void, Never>()
var name: String {
willSet { willChange.send() }
}
var counter: Int = 0 {
willSet { willChange.send() }
}
init(name: String) {
self.name = name
}
}
struct ContentView : View {
let items = [
ItemViewModel(name: "Item A"),
ItemViewModel(name: "Item B"),
ItemViewModel(name: "Item C")
]
@State var contentViewUpdater = 0
var body: some View {
NavigationView {
VStack {
Button("Update ContentView: \(contentViewUpdater)") {
self.contentViewUpdater += 1
}
List(items) { model in
NavigationLink(destination: DetailView(model: model)) {
Text(model.name)
}
}
}
}
}
}
struct DetailView : View {
@ObjectBinding var model: ItemViewModel
var body: some View {
let name = model.name
let counter = model.counter
return VStack {
Text("Counter for \(name): \(counter)")
Button("Increase counter") {
self.model.counter += 1
}
}
}
}
PlaygroundPage.current.liveView = UIHostingController(rootView: ContentView())
PlaygroundPage.current.needsIndefiniteExecution = true
Upvotes: 3
Reputation: 40519
Your views should not have to be aware of whether SwiftUI regenerates the view or not. In your case, I think you have to change the way you have laid out your model.
There's two approaches I would take in your case:
I think the first option: having a single model, is better. But to illustrate my second point, here's a possible implementation:
import SwiftUI
import Combine
struct Item: Identifiable {
let id = UUID()
let model: DetailViewModel
init(name: String) {
self.model = DetailViewModel(item: name)
}
}
struct ContentView: View {
@State private var items = [Item(name: "Item A"), Item(name: "Item B"), Item(name: "Item C")]
@State var contentViewUpdater = 0
var body: some View {
NavigationView {
VStack {
Button("Update ContentView: \(contentViewUpdater)") {
self.contentViewUpdater += 1
}
List(items, id: \.id) { item in
NavigationLink(destination: DetailView(model: item.model)) {
Text(item.model.item)
}
}
}
}
}
}
final class DetailViewModel: ObservableObject {
let item: String
@Published var counter = 0
init(item: String) {
self.item = item
}
}
struct DetailView: View {
@ObservedObject var model: DetailViewModel
var body: some View {
VStack {
Text("Counter for \(model.item): \(model.counter)")
Button("Increase counter") {
self.model.counter += 1
}
}
}
}
Upvotes: 1