The UserDefaults publisher allows you subscribe even though the property is not compliant:
enum Direction: String {
case north, south, east, west
}
extension UserDefaults {
var direction: Direction {
get { Direction(rawValue: string(forKey: "direction") ?? "") ?? .north }
set { set(newValue.rawValue, forKey: "direction") }
}
}
let defaults = UserDefaults.standard
var cancellable = Set<AnyCancellable>()
defaults
.publisher(for: \.direction)
.sink { print("Sink: \($0)") } // Crash
.store(in: &cancellable)
defaults.direction = .east
cancellable.removeAll()
The above code compiles and gives no warning either. At runtime, it crashes in the sink:
error: Execution was interrupted, reason: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0).
The process has been left at the point where it was interrupted, use "thread return -x" to return to the state before expression evaluation.
Fatal error: Could not extract a String from KeyPath Swift.ReferenceWritableKeyPath<__C.NSUserDefaults, __lldb_expr_43.Direction>: file Foundation/NSObject.swift, line 124
I'll probably end up creating a private string version and expose it through a publisher like this:
extension UserDefaults {
@objc private var directionRaw: String? {
get { string(forKey: "direction") }
set { set(newValue, forKey: "direction") }
}
var directionPublisher: AnyPublisher<Direction, Never> {
publisher(for: \.directionRaw)
.compactMap { [weak self] _ in self?.direction }
.eraseToAnyPublisher()
}
var direction: Direction {
get { Direction(rawValue: directionRaw ?? "") ?? .north }
set { directionRaw = newValue.rawValue }
}
}
defaults
.directionPublisher
.sink { print("Sink: \($0)") }
.store(in: &cancellable)
The problem is anyone can still do this in the codebase:
defaults
.publisher(for: \.direction)
.sink { print("Sink: \($0)") } // Crash
.store(in: &cancellable)
Is there a way to prevent anyone from doing this, or hopefully there's a better way to do this altogether?