skip to Main Content

I have following observable made to keep track of a custom modal state

import SwiftUI


@Observable class ModalObservable {
  // State
  var view = ModalViewType.Profile
  var open = false
  
  // Actions
  func show(view: ModalViewType) {
    self.open = true
    self.view = view
  }
  
  func hide() {
    self.open = false
  }
}

Instead of having each state value as a separate variable, I want to make a var state = ModalState() one, where ModalState will be a struct containing each individual value.

I’m unable to find any docs on how this could affect ui re-rendering logic, mainly will my ui re-render when anything in this struct changes or only when nested value that my ui is accessing changes?

As example, if my ui only uses ModalObservable.state.open will it re-render when view changes?

2

Answers


  1. First, let’s be clear about what "re-render" means. If you mean "creates an entirely new view", then this will only happen if the identity of the view changes. If your view’s identity does not depend on ModalObservable, then how you change ModelObservable is not relevant. This can be experimentally detected by onAppear.

    If you mean "updates an existing view", then this depends on the view’s dependencies. SwiftUI creates a dependency graph from your views and the states/bindings/environments etc that the views use, and updates the view if any of the dependencies change. This can be experimentally detected by having body produce a side effect (though body is also called when the view is first created).

    Here is some code that you can use to experiment:

    struct ContentView: View {
        @State var model = ModalObservable()
        
        var body: some View {
            let _ = print("ContentView updates")
            UpdateDetector(observable: model)
            Button("Change Open") {
                model.changeOpen()
            }
            Button("Change View") {
                model.changeView()
            }
        }
    }
    
    struct UpdateDetector: View {
        let observable: ModalObservable
        
        var body: some View {
            let _ = print("UpdateDetector updates")
            Text(observable.state.open ? "Open" : "Close")
        }
    }
    
    @Observable class ModalObservable {
      var state = ModelState()
      func changeView() {
        self.state.view = UUID().uuidString
      }
      
      func changeOpen() {
        self.state.open.toggle()
      }
    }
    
    struct ModelState {    
        var view = "Foo"
        var open = false
    }
    

    Here SwiftUI has determined that UpdateDetector depends on observable.state, so it updates whenever it observes a change in observable.state, i.e. when either of the buttons is pressed. On the other hand, ContentView does not update, because it does not depend on any observable property.

    Note that observable.state.open and observable.state.view does not count as two separate observable properties, because the @Observable macro only adds @ObservationTracked to state.

    Here’s another example, this time with Bindings.

    struct ContentView: View {
        @State var model = ModalObservable()
        
        var body: some View {
            let _ = print("ContentView updates")
            UpdateDetector(open: $model.state.open)
            Button("Change Open") {
                model.changeOpen()
            }
            Button("Change View") {
                model.changeView()
            }
        }
    }
    
    struct UpdateDetector: View {
        @Binding var open: Bool
        
        var body: some View {
            let _ = print("UpdateDetector updates")
            Text(open ? "Open" : "Close")
        }
    }
    

    Here, UpdateDetector only depends on the open binding, so pressing "Change View" does not update it, but "Change Open" does. On the other hand, ContentView now also depends on the open binding, since a binding is two-way. Pressing "Change Open" updates ContentView too.


    If you do want observable.state.open and observable.state.view to be tracked separately, you can change ModelState to also be an @Observable class, though this also changes semantics.

    @Observable
    class ModelState {
        var view = "Foo"
        var open = false
    }
    
    Login or Signup to reply.
  2. If you use @Observable modifier it’s almost the same as you inerhit from ObservableObject

    Difference is:

    @Observable                                                 | ObservableObject
    -------------------------------------------------------------------------------------------
    @Observable class MyModel { }                               | class MyModel: ObservableObject { }
    
    No need to use @Published at all                            | Need to use @Published on each var that must be cause of refresh view
    
    In View: @State var model                                   | In View: @StateObject var model; @ObservedObject var model
    
    Change of public var (struct) will re-render view           | Only if @Published
                              (same in case of change of property of struct var)
    
    Change of private var (struct) will NOT re-render view      | Will NOT re-render view
    
    Change of private(set) var (struct) will re-render view     | Only if @Published
    
    Change of public var (class) will NOT re-render view        | Will NOT re-render view even if it is @Published
                              (same in case of change of property of class var)
    

    To perform refresh in case of property of your model is class you need to use ‘@Observable’ modifier on this class or inerhit class from ObservableObject

    AND(!!!!!) assign it to extra view’s var like here:

    struct SomeView : View {
        @ObservedObject var model : SomeViewModel
        @ObservedObject var someClassValue: MyClass
        
        init(model: SomeViewModel) {
            self.model = model
        
            //as this is class we must do it observable and assign into view manually
            self.someClassValue = model.someClassValue
        }
    
        var body: some View {
             //here we can use model.someStructValue directly
    
             // or we can use local someClassValue taken from VIEW, BUT NOT value from model
    
        }
    
    }
    
    class SomeViewModel : ObservableObject {
        @Published var someStructValue: Bool = false
        var someClassValue: MyClass = MyClass() //myClass : ObservableObject
    
    }
    

    Usage of @Observable modifier make code more "clean", but it is causes some extra view refreshes in some cases that you can skip in case of usage inerhitance from ObservableObject. Ofc you can use in some cases @Observable in some cases ObservableObject, but it will create different codestyle in single project.

    My opinion: SwiftUI is not lightweight UI system, so better do not use @Observable as it makes it even slower because of extra refreshes.

    My choice is to use ObservableObject

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search