Skip to content

Instantly share code, notes, and snippets.

@KoCMoHaBTa
Forked from DevAndArtist/CustomStateObject.swift
Created December 9, 2021 12:40
Show Gist options
  • Save KoCMoHaBTa/e948939df605671779eb95a81de6f9be to your computer and use it in GitHub Desktop.
Save KoCMoHaBTa/e948939df605671779eb95a81de6f9be to your computer and use it in GitHub Desktop.
Custom `StateObject` which should be backwards compatible with iOS 13.
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