I'm trying to mimic the Combine @Published property wrapper. My end goal is to create a new custom property wrapper (e.g. @PublishedAppStorage) of @Published with a nested @AppStorage. So I've started just by trying to mimic the @Published.
My problem that it crashes when accessing the original value from within the sink block with the error:
Thread 1: Simultaneous accesses to 0x600000103328, but modification requires exclusive access
I've spent days trying to find a way.
Here is my custom @DMPublished:
@propertyWrapper
struct DMPublished<Value> {
private let subject:CurrentValueSubject<Value, Never>
init(wrappedValue: Value) {
self.wrappedValue = wrappedValue
self.subject = CurrentValueSubject(wrappedValue)
}
var wrappedValue: Value {
willSet {
subject.send(newValue)
}
}
var projectedValue: AnyPublisher<Value, Never> {
subject.eraseToAnyPublisher()
}
}
The ObservableObject defining my properties:
import Combine
public class DMDefaults: ObservableObject {
static public let shared = DMDefaults()
private init(){}
@Published public var corePublishedString = "dd"
@DMPublished public var customPublishedString = "DD"
}
And here is my test function:
public func testSink()
{
let gdmDefaults = DMDefaults.shared
gdmDefaults.corePublishedString = "ee"; gdmDefaults.customPublishedString = "EE"
gdmDefaults.corePublishedString = "ff"; gdmDefaults.customPublishedString = "FF"
let coreSub = gdmDefaults.$corePublishedString.sink { (newVal) in
print("coreSub: oldVal=\(gdmDefaults.corePublishedString) ; newVal=\(newVal)")
}
let custSub = gdmDefaults.$customPublishedString.sink { (newVal) in
print("custSub: oldVal=\(gdmDefaults.customPublishedString) ; newVal=\(newVal)") // **Crashing here**
}
gdmDefaults.corePublishedString = "gg"; gdmDefaults.customPublishedString = "GG"
}
Will appreciate any help here... thanks...
Thanks for the direction from David, I've managed to get it working. I'm posting here my final Property Wrapper in case anyone finds it helpful.
import SwiftUI
import Combine
@propertyWrapper
public struct PublishedAppStorage<Value> {
// Based on: https://github.com/OpenCombine/OpenCombine/blob/master/Sources/OpenCombine/Published.swift
@AppStorage
private var storedValue: Value
private var publisher: Publisher?
internal var objectWillChange: ObservableObjectPublisher?
/// A publisher for properties marked with the `@Published` attribute.
public struct Publisher: Combine.Publisher {
public typealias Output = Value
public typealias Failure = Never
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Input == Value, Downstream.Failure == Never
{
subject.subscribe(subscriber)
}
fileprivate let subject: Combine.CurrentValueSubject<Value, Never>
fileprivate init(_ output: Output) {
subject = .init(output)
}
}
public var projectedValue: Publisher {
mutating get {
if let publisher = publisher {
return publisher
}
let publisher = Publisher(storedValue)
self.publisher = publisher
return publisher
}
}
@available(*, unavailable, message: """
@Published is only available on properties of classes
""")
public var wrappedValue: Value {
get { fatalError() }
set { fatalError() }
}
public static subscript<EnclosingSelf: ObservableObject>(
_enclosingInstance object: EnclosingSelf,
wrapped wrappedKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Value>,
storage storageKeyPath: ReferenceWritableKeyPath<EnclosingSelf, PublishedAppStorage<Value>>
) -> Value {
get {
return object[keyPath: storageKeyPath].storedValue
}
set {
// https://stackoverflow.com/a/59067605/14314783
(object.objectWillChange as? ObservableObjectPublisher)?.send()
object[keyPath: storageKeyPath].publisher?.subject.send(newValue)
object[keyPath: storageKeyPath].storedValue = newValue
}
}
// MARK: - Initializers
// RawRepresentable
init(wrappedValue: Value, _ key: String, store: UserDefaults? = nil) where Value : RawRepresentable, Value.RawValue == String {
self._storedValue = AppStorage(wrappedValue: wrappedValue, key, store: store)
}
// String
init(wrappedValue: String, _ key: String, store: UserDefaults? = nil) where Value == String {
self._storedValue = AppStorage(wrappedValue: wrappedValue, key, store: store)
}
// Data
init(wrappedValue: Data, _ key: String, store: UserDefaults? = nil) where Value == Data {
self._storedValue = AppStorage(wrappedValue: wrappedValue, key, store: store)
}
// Int
init(wrappedValue: Int, _ key: String, store: UserDefaults? = nil) where Value == Int {
self._storedValue = AppStorage(wrappedValue: wrappedValue, key, store: store)
}
// URL
init(wrappedValue: URL, _ key: String, store: UserDefaults? = nil) where Value == URL {
self._storedValue = AppStorage(wrappedValue: wrappedValue, key, store: store)
}
// Double
init(wrappedValue: Double, _ key: String, store: UserDefaults? = nil) where Value == Double {
self._storedValue = AppStorage(wrappedValue: wrappedValue, key, store: store)
}
// Bool
init(wrappedValue: Bool, _ key: String, store: UserDefaults? = nil) where Value == Bool {
self._storedValue = AppStorage(wrappedValue: wrappedValue, key, store: store)
}
}
And this how it is used:
public class MyDefaults: ObservableObject
{
@PublishedAppStorage("useVerboseLog")
public var useVerboseLog: Bool = true
}