@Observable object - How to initialize it only once for identical views

@Observable object - How to initialize it only once for identical views

Muukii

-

Oct 9, 2023

https://developer.apple.com/forums/thread/739163

iOS 17 introduced @Observable. that's an effective way to implement a stateful model object.


Seems there is no way to associate instances with views

However, we are not able to use @StateObject as the model object does not have ObservableObject protocol. An advantage of using @StateObject is to make the object initialized once only for the view. It will keep going on until the view identifier is changed.

I put some examples. We have an Observable implemented object.

@Observable final class Controller { ... }

then using like this

struct MyView: View {

  let controller: Controller

  // or
  init(value: Value) {
    self.controller = .init(value: value)
  }

  // or
  init(controller: Controller) {
    self.controller = controller
  }

}

This case causes a problem in that the view body uses the passed controller anyway.
Even passed a different controller, views use it.

Plus, in the case of initializing a controller takes expensive costs, which decreases performance.

so how do we manage this kind of use case?

A workaround - making a wrapper view that provides instances for views

anyway I made a utility view that provides an observable object lazily.

public struct ObjectProvider<Object, Content: View>: View {

  @State private var object: Object?

  private let _objectInitializer: () -> Object
  private let _content: (Object) -> Content

  public init(object: @autoclosure @escaping () -> Object, @ViewBuilder content: @escaping (Object) -> Content) {
    self._objectInitializer = object
    self._content = content
  }

  public var body: some View {
    Group {
      if let object = object {
        _content(object)
      } else {
        Color.clear
          .onAppear {
            assert(object == nil, "it should not be running twice or more.")
            guard object == nil else { return }
            object = _objectInitializer()
          }
      }
    }
  }

}


ObjectProvider(object: Controller() { controller in

  MyView(controller: controller)

}


I hope it should be better ways rather than this workaround I made.


Updated 2023-12-21


import SwiftUI

@propertyWrapper
struct ObservableEdge<O: Observable>: DynamicProperty {

  @State private var box: Box<O> = .init()

  var wrappedValue: O {
    if let value = box.value {
      return value
    } else {
      box.value = factory()
      return box.value!
    }
  }

  private let factory: () -> O

  init(wrappedValue factory: @escaping @autoclosure () -> O) {
    self.factory = factory
  }

  private final class Box<Value> {
    var value: Value?
  }

}