-
-
Save Amzd/8f0d4d94fcbb6c9548e7cf0c1493eaff to your computer and use it in GitHub Desktop.
| import Combine | |
| import PublishedObject // https://github.com/Amzd/PublishedObject | |
| import SwiftUI | |
| /// A property wrapper type that instantiates an observable object. | |
| @propertyWrapper | |
| public struct StateObject<ObjectType: ObservableObject>: DynamicProperty | |
| where ObjectType.ObjectWillChangePublisher == ObservableObjectPublisher { | |
| /// Wrapper that helps with initialising without actually having an ObservableObject yet | |
| private class ObservedObjectWrapper: ObservableObject { | |
| @PublishedObject var wrappedObject: ObjectType? = nil | |
| init() {} | |
| } | |
| private var thunk: () -> ObjectType | |
| @ObservedObject private var observedObject = ObservedObjectWrapper() | |
| @State private var state = ObservedObjectWrapper() | |
| public var wrappedValue: ObjectType { | |
| if state.wrappedObject == nil { | |
| // There is no State yet so we need to initialise the object | |
| state.wrappedObject = thunk() | |
| // and start observing it | |
| observedObject.wrappedObject = state.wrappedObject | |
| } else if observedObject.wrappedObject == nil { | |
| // Retrieve the object from State and observe it in ObservedObject | |
| observedObject.wrappedObject = state.wrappedObject | |
| } | |
| return state.wrappedObject! | |
| } | |
| public var projectedValue: ObservedObject<ObjectType>.Wrapper { | |
| ObservedObject(wrappedValue: wrappedValue).projectedValue | |
| } | |
| public init(wrappedValue thunk: @autoclosure @escaping () -> ObjectType) { | |
| self.thunk = thunk | |
| } | |
| public mutating func update() { | |
| // Not sure what this does but we'll just forward it | |
| _state.update() | |
| _observedObject.update() | |
| } | |
| } |
Yes, update function should reinitialise the observed object. (as it gets reset whenever StateObject.init is called)
I have found a "hacky solution" to detect this case: ObservedObject has an internal property _seed which is some internal state.
Whenever the value is 1 our StateObject should recreate a new object.
public mutating func update() {
// Not sure what this does but we'll just forward it
_state.update()
_observedObject.update()
// HACK! We rely on the internal _seed variable of `ObservedObject` to learn when we should initialize
let mirror = Mirror(reflecting: _observedObject)
guard let seed = mirror.descendant("_seed") as? Int else {
return
}
if seed == 1 {
state.wrappedObject = thunk()
observedObject.wrappedObject = state.wrappedObject
}
}Correct me if I'm wrong, but since we call the _state.update() that should reinit its ObservedObjectWrapper causing state.wrappedObject and observedObject.wrappedObject to not be the same object? @pd95 @malhal
If that's the case I think the best would be to just change the wrappedValue getter to always update the observedObject.wrappedObject?
public var wrappedValue: ObjectType {
if state.wrappedObject == nil {
// There is no State yet so we need to initialise the object
state.wrappedObject = thunk()
- }
- if observedObject.wrappedObject == nil {
// Retrieve the object from State and observe it in ObservedObject
observedObject.wrappedObject = state.wrappedObject
}
return state.wrappedObject!
}I know the init happens when you get wrappedValue vs when update is called but I don't like relying on undocumented behaviour.
you are a legend!
[Is there a way to conditionally use @StateObject while targeting iOS 13 but use apple's @StateObject in iOS 14
If you are using Combine's ObservableObject as the @StateObject you could use Async/await to replace the functionality and that is backwards compatible with iOS 13. Could be tricky without the task(priority:_:) modifier though.
[Is there a way to conditionally use @StateObject while targeting iOS 13 but use apple's @StateObject in iOS 14
try this?
@available(iOS 13, obsoleted: 14)@available(iOS 13, obsoleted: 14)
@calvingit this not work
I insert a break point, it also called iOS 13 method...
Update func is supposed to init the object