-
-
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() | |
} | |
} |
Update func is supposed to init the object
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...
Currently don’t have access to a Mac but you can probably do a couple
if #available
checks inside a separate property wrapper to switch between wrappers (you can do SwiftUI.StateObject to get the SwiftUI type)