Lazy init of @State objects using new Observable protocol

Hi,

Previously, we would conform model objects to the ObservableObject protocol and use the @StateObject property wrapper when storing them to an owned binding in a View.

Now, if I understand correctly, it is recommended that we use the new @Observable macro/protocol in place of ObservableObject and use the @State property wrapper rather than @StateObject. This is my understanding from documentation articles such as Migrating from the Observable Object protocol to the Observable macro.

However, the StateObject property wrapper has an initialiser which takes an autoclosure parameter:

extension StateObject {
    public init(wrappedValue thunk: @autoclosure @escaping () -> ObjectType)
}

This is an extremely important initialiser for state objects that are expensive to allocate. As far as I can tell, the @State property wrapper lacks an equivalent initialiser.

What is the recommended migration strategy for objects which made use of this on StateObject?

Thanks

try this:

@MainActor
@propertyWrapper
public struct LazyState<T: Observable>: @preconcurrency DynamicProperty {
  @State private var holder: Holder

  public var wrappedValue: T {
    holder.wrappedValue
  }

  public var projectedValue: Binding<T> {
    return Binding(get: { wrappedValue }, set: { _ in })
  }

  public func update() {
    guard !holder.onAppear else { return }
    holder.setup()
  }

  public init(wrappedValue thunk: @autoclosure @escaping () -> T) {
    _holder = State(wrappedValue: Holder(wrappedValue: thunk()))
  }
}

extension LazyState {
  final class Holder {
    private var object: T!
    private let thunk: () -> T
    var onAppear = false
    var wrappedValue: T {
      object
    }

    func setup() {
      object = thunk()
      onAppear = true
    }

    init(wrappedValue thunk: @autoclosure @escaping () -> T) {
      self.thunk = thunk
    }
  }
}

// Demo
struct LinkViewUsingLazyState: View {
  @LazyState var object = TestObservationObject()
  var body: some View {
    Text("LazyState")
    Text(object.name)
    @Bindable var o = object
    TextField("hello", text: $o.name)
    Button("change") {
      object.name = "\(Int.random(in: 0 ... 1000))"
    }
  }
}

@Observable
class TestObservationObject {
  init() {
    print("init")
  }

  var name = "abc"
}

This is an extremely important initialiser for state objects that are expensive to allocate. As far as I can tell, the @State property wrapper lacks an equivalent initialiser. What is the recommended migration strategy for objects which made use of this on StateObject?

You can mark the property as an optional and initialize the value just before its needed. Keep in mind that SwiftUI creates only one instance of the state for each container instance that you declare.

That said, you can file an enhancement request

If you'd like the engineering team to consider adding a Lazy state API, please file an enhancement request using Feedback Assistant. Once you file the request, please post the FB number here.

If you're not familiar with how to file enhancement requests, take a look at Bug Reporting: How and Why?

Lazy init of @State objects using new Observable protocol
 
 
Q