-
-
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() | |
} | |
} |
Ah, add import SwiftUI
I think I have @_exported import SwiftUI
somewhere in my project so I did not get those errors
Builds now and seems to work okay. Is there a way for me to make the app use the original StateObject when available and fallback to your patch if it is not? I was thinking of renaming the library to prevent override but I'm not sure how I can dynamically change.
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)
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...
Errors out isn't even the word.
Am I doing something wrong or is it really troubled?