Skip to content

Instantly share code, notes, and snippets.

@Amzd
Last active March 27, 2024 20:14
Show Gist options
  • Star 26 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Amzd/8f0d4d94fcbb6c9548e7cf0c1493eaff to your computer and use it in GitHub Desktop.
Save Amzd/8f0d4d94fcbb6c9548e7cf0c1493eaff to your computer and use it in GitHub Desktop.
StateObject that works in iOS 13
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()
}
}
@Amzd
Copy link
Author

Amzd commented Nov 1, 2021

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.

@TuenTuenna
Copy link

you are a legend!

@BugMonkey
Copy link

[Is there a way to conditionally use @StateObject while targeting iOS 13 but use apple's @StateObject in iOS 14

@malhal
Copy link

malhal commented Mar 15, 2022

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.

@calvingit
Copy link

[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)

@eccentricyan
Copy link

eccentricyan commented May 17, 2022

@available(iOS 13, obsoleted: 14)

@calvingit this not work
I insert a break point, it also called iOS 13 method...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment