1

Please see the MRE below.
Changing UserDefault suite to a custom one doesn't update the subscription beyond the first value.

public extension UserDefaults {
    @objc dynamic var value1: Int {
        get { integer(forKey: "value1") }
        set { set(newValue, forKey: "value1") }
    }

    static var other: UserDefaults {
        UserDefaults(suiteName: "other-defaults")!
    }
}

struct ContentView: View {
    private let sub = UserDefaults.other.publisher(for: \.value1).sink { print("SUB", $0) }

    var body: some View {
        Button("Add") {
            UserDefaults.other.value1 += 1
            debugPrint("SET", UserDefaults.other.value1)
        }
        .onReceive(UserDefaults.other.publisher(for: \.value1)) {
            debugPrint("UI", $0)
        }
    }
}

Only the SwiftUI onReceive subscription is working.

SUB 0
"UI" 0
"SET" 1
"UI" 1
"SET" 2
"UI" 2

It works (though with multiple calls) if I set the standard UserDefaults suite.

struct ContentView2: View {
    private let sub = UserDefaults.standard.publisher(for: \.value1).sink { print("SUB", $0) }

    var body: some View {
        Button("Add") {
            UserDefaults.standard.value1 += 1
            debugPrint("SET", UserDefaults.standard.value1)
        }
        .onReceive(UserDefaults.standard.publisher(for: \.value1)) {
            debugPrint("UI", $0)
        }
    }
}
SUB 0
"UI" 0
SUB 1
SUB 1
"SET" 1
"UI" 1
"UI" 1
SUB 2
SUB 2
"SET" 2
"UI" 2
"UI" 2
Martin
  • 1,112
  • 1
  • 11
  • 31

1 Answers1

1

I came up with a solution to this, but the explanation is a bit wonky.

My initial suspicion was correct. With your implementation of other in the extension you are creating an new UserDefaults store every time you are accessing the var. It seems you need to hold on to this store or it will go out of scope and you won´t receive any values. The UserDefaults.standard is a shared instance, according to the documentation.

Here comes the strange part: I could only speculate why the .onReceive wrapper works as this is implementation detail of SwiftUI. Also why using the .standard store is emitting twice (btw. using a single .other instance shows the same result) is beyond me.

This implementation should show the desired output:

struct ContentView: View {
    private let otherSuite: UserDefaults
    private let sub: AnyCancellable

    init() {
        print("init")
        otherSuite = UserDefaults.other
        sub = otherSuite.publisher(for: \.value1).sink { print("SUB", $0) }
    }
    
    var body: some View {
        Button("Add") {
            // don´t use ohterSuite here or your sink will run twice
            UserDefaults.other.value1 += 1
            debugPrint("SET", otherSuite.value1)
        }
        // the same here
        .onReceive(UserDefaults.other.publisher(for: \.value1)) {
            debugPrint("UI", $0)
        }
    }
}

Print:

init
SUB 0
"UI" 0
SUB 1
"SET" 1
"UI" 1
SUB 2
"SET" 2
"UI" 2
burnsi
  • 6,194
  • 13
  • 17
  • 27
  • omg, you're right. Using a shared instance for `other` as well solved my initial problem. – Martin Nov 16 '22 at 12:57
  • 1
    As for the other two, yes quite the mystery. I think the double posting was mentioned in another question here that I found along the way, but also inconclusive... if I find it again, I'll link it – Martin Nov 16 '22 at 12:58