Compile safety for UserDefaults publisher instead of crash?

Viewed 371

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?

0 Answers
Related