/**
Using `@Observable` causes this view model to initialise every time the `increment()` is called.
However marking `index` as `@ObservationIgnored` doesn't not cause it to init , which is the expected behaviour.
Using `@ObservedObject`, `ObservableObject` and `@Published` pattern causes correct behaviour.
There is also a bug when there is a //comment after the class name declaration that uses `@Observable`, then the macro doesn't compile.
*/
@Observable
//public class Test2328ViewModel /*k <-- uncomment this line's initial comment , works fine*/
//public class Test2328ViewModel //k <-- uncomment this line's initial comment, for compile failure
public class Test2328ViewModel
{
typealias T = String//VoltronViewModel
// @Published
var models : [T] = ["observable causes the view to init every time" , "i don't see this string because of which"]
// @Published // @Published works fine on `index`
// @ObservationIgnored // Using this causes correct behavious
var index : Int = 0
// @Published
var pogmodel : T!
init() {
pogmodel = models[index]
print("init is called , so the index resets to 0. But isn't called if index is marked as @ObservationIgnored. Then the code behaves as intended.")
}
func increment() {
var x = index + 1
if x >= models.count {
x = 0
}
index = x
pogmodel = models[index]
}
}
struct Test2328View: View {
// @ObservedObject
var model : Test2328ViewModel
var body : some View {
VStack(spacing : 0.0) {
Text("\(model.pogmodel)")
Text("\(model.index)")
Button(action: {
model.increment()
}, label: {
Image(systemName: "arrowtriangle.left.fill").font(.system(size: 20.0))
})
}
}
}
struct Test8238ParentView : View {
init() {
print("super init called")
}
var body : some View{
Test2328View(model: .init())
}
}
#Preview {
Test8238ParentView()
}
I believe correct way to use it with the old observation machinery would be to use StateObject instead of creating a new model. You are just lucky the view is not recreated.
Corrected app
import SwiftUI
/* import Observation */
/* @Observable */ class TestViewModel: ObservableObject {
@Published var models = ["one" , "two"]
@Published /* @ObservationIgnored */ var index: Int = 0
@Published var pogmodel: String! = nil
init() {
pogmodel = models[index]
print("init!!!")
}
func increment() {
index = (index + 1) % models.count
pogmodel = models[index]
}
}
struct TestView: View {
@StateObject var model = TestViewModel()
var body: some View {
VStack(spacing : 0.0) {
Text("\(model.pogmodel)")
Text("\(model.index)")
Button("increment") {
model.increment()
}
}
}
}
struct ParentView: View {
var body: some View{
TestView()
}
}
#Preview {
ParentView()
}
@main struct TheApp: App {
var body: some Scene {
WindowGroup { ParentView() }
}
}
Having said that, I don't know the equivalent of StateObject in the new observation machinery.
Well found. This is a separate issue. Minimal example:
@Observable class C //
{}
// 🛑 Error: Expected '>' to complete generic argument
I think it is an Observable issue. In the code below I am using pogModel as computed property instead of setting a new property and it works fine (view isn't reinitialised).
public class Test2328ViewModel
{
// @ObservationIgnored // Using this causes correct behavious
var index : Int = 0
var pgmodels : [String] = ["observable causes the view to init every time" ,
"i don't see this string because of which"]
var pogmodel : String
{
pgmodels[index]
}
init() {
// self.pogmodel = pgmodels[index]
print("init is called , so the index resets to 0. But isn't called if index is marked as @ObservationIgnored. Then the code behaves as intended.")
}
func increment() {
var x = index + 1
if x >= pgmodels.count {
x = 0
}
index = x
// pogmodel = pgmodels[index]
}
}
Think about it this way: views are value types. They could be initialised on a whim whenever SwiftUI wants so (or thinks it wants so) and it could happen even if nothing is changing on the screen. Ditto for the body callouts - SwiftUI tries to call "body" as rare as possible but there is no guarantee.
This is a subtle gotcha of @Observable and unfortunately it is working as expected. By accessing an observed field in the object's init, you can unwittingly cause the parent view to observe that state.
Here is a very small reproduction of the problem:
import SwiftUI
@Observable
class Feature {
var isPresented = false
init(isPresented: Bool = false) {
self.isPresented = isPresented
_ = self.isPresented // 👈 Accessing any observable field "breaks" observation.
}
}
struct FeatureView: View {
@Bindable var model: Feature
var body: some View {
Button("Present") {
self.model.isPresented = true
}
.sheet(isPresented: self.$model.isPresented) {
Text("Hi")
}
}
}
@main
struct FeedbacksApp: App {
var body: some Scene {
WindowGroup {
FeatureView(model: Feature())
}
}
}
The mere act of accessing an observable field inside Feature's init causes the parent view to observe that field, and so when the field changes the parent re-computes the body, thus leading to a new Feature object being created and passed to FeatureView.
Using @State can fix it because that prevents the parent from passing another object to the child. Another workaround would be to use the underscored properties in the init instead: