-
-
Save KoCMoHaBTa/e948939df605671779eb95a81de6f9be to your computer and use it in GitHub Desktop.
Custom `StateObject` which should be backwards compatible with iOS 13.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import SwiftUI | |
import Combine | |
/// The idea is to use State to create a storage object which will be | |
/// cached and restored by the framework. Then during the update phase we | |
/// re-inject the object into a nested ObservableObject and subscribe to the | |
/// ObjectType’s objectWillChange to forward to the _MessageForwarder’s | |
/// objectWillChange which is already subscribed by the framework as it’s an | |
/// inner dynamic property of our custom PW which is also a dynamic property. | |
@propertyWrapper | |
public struct CustomStateObject<ObjectType>: | |
DynamicProperty | |
where | |
ObjectType: ObservableObject | |
{ | |
let _thunk: () -> ObjectType | |
@State | |
var _isObjectInitialized = false | |
final class _RestorableBox { | |
var object: ObjectType? | |
} | |
@State | |
var _box = _RestorableBox() | |
final class _MessageForwarder: ObservableObject { | |
let objectWillChange = ObservableObjectPublisher() | |
var subscription: AnyCancellable? | |
var object: ObjectType? { | |
didSet { | |
// Subscribe to the object only if needed. | |
if let object = object, object !== oldValue { | |
subscription = object.objectWillChange.sink { [weak self] _ in | |
self?.objectWillChange.send() | |
} | |
} | |
} | |
} | |
} | |
@ObservedObject | |
var _messageForwarder = _MessageForwarder() | |
public init(wrappedValue thunk: @autoclosure @escaping () -> ObjectType) { | |
self._thunk = thunk | |
} | |
public var wrappedValue: ObjectType { | |
get { | |
if let object = _messageForwarder.object { | |
return object | |
} else { | |
fatalError( | |
""" | |
BUG: This should never happen as `update` is guaranteed to mutate \ | |
before you're allowed to access `wrappedValue` property. | |
""" | |
) | |
} | |
} | |
} | |
public var projectedValue: ObservedObject<ObjectType>.Wrapper { | |
if let object = _messageForwarder.object { | |
let wrapper = ObservedObject(wrappedValue: object) | |
return wrapper.projectedValue | |
} else { | |
fatalError( | |
""" | |
BUG: This should never happen as `update` is guaranteed to mutate \ | |
before you're allowed to access `projectedValue` property. | |
""" | |
) | |
} | |
} | |
public func update() { | |
// This mutation should only happen once for the lifetime of the view. | |
if _isObjectInitialized == false { | |
_box.object = _thunk() | |
// We cannot mutate this directly or we bump into this runtime warning: | |
// "Modifying state during view update, this will cause undefined | |
// behavior." | |
DispatchQueue.main.async { | |
_isObjectInitialized = true | |
} | |
} | |
// Re-inject the object into the message forwarder. | |
if let object = _box.object { | |
_messageForwarder.object = object | |
} | |
} | |
} | |
// Test code | |
class Model: ObservableObject { | |
@Published | |
var number = 0 | |
} | |
struct ContentView: View { | |
@State | |
var number = 0 | |
@State | |
var flag = true | |
var body: some View { | |
VStack { | |
Text(String("\(number)")) | |
// The idea is survive the be re-initialization of `V` through this call. | |
Button("number + 1") { | |
number += 1 | |
} | |
if flag { | |
V() | |
} | |
Button("toogle") { | |
flag.toggle() | |
} | |
} | |
} | |
} | |
struct V: View { | |
@CustomStateObject | |
var foo = Model() | |
@StateObject | |
var bar = Model() | |
var body: some View { | |
VStack { | |
Text(String("\(foo.number)")) | |
Button("foo + 1") { | |
foo.number += 1 | |
} | |
Text(String("\(bar.number)")) | |
Button("bar + 1") { | |
bar.number += 1 | |
} | |
} | |
} | |
} | |
struct ContentView_Previews: PreviewProvider { | |
static var previews: some View { | |
ContentView() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment