Tom Lieber's Microblog

Very simply binding an iCloud key/value store item in SwiftUI

I had a @State variable in a SwiftUI view that I wanted to persist using iCloud’s key/value storage. Seemed like it should have been easy… and it was!

The gist is: store the value in a Binding with custom get and set. In my case, the value is an enum implementing Codable using strategies Dmitrii Ivanov summarizes here, so I just replaced this:

@State private var myvar: MyEnum

with this:

private var myvar: Binding<MyEnum> {
    Binding<MyEnum>(get: {
        if let data = NSUbiquitousKeyValueStore().data(forKey: "myvar") {
            if let state = try? PropertyListDecoder().decode(MyEnum.self, from: data) {
                return state
            }
        }
        return MyEnum.someDefaultSetting
    }, set: {
        if let encoded = try? PropertyListEncoder().encode($0) {
            let store = NSUbiquitousKeyValueStore()
            store.set(encoded, forKey: "myvar")
            store.synchronize()
        }
    })
}

It’s begging to be encapsulated for reuse, but I’ve only ever needed it once so far. :)

Unfortunately, you have to change all the code that accesses the value of myvar and gets bindings for myvar. Now self.myvar refers to the binding and you must use self.myvar.wrappedValue to refer to the current value. But the substitution is simple:

Access valueAccess binding
@Stateself.myvarself.$myvar
Bindingself.myvar.wrappedValueself.myvar

Creating a binding this way is useful in general to perform custom actions when a @State’s value changes, which you can’t do by defining didSet on a @State variable. If you try using didSet, it will be called when you set the value directly, like self.myvar = foo, but it will not be called if the value is set via the self.$myvar binding, such as when you bind it to an input widget.


Addendum: I’ve found that a Binding isn’t a drop-in replacement for @State. A view will redraw in response to a change in @State, but will not do so for a custom Binding. So while the code above is useful, you still need a @State variable for a reactive UI. The easiest way I’ve found to do that is to always use the custom Binding for mutations, such as by editable controls (not the @State’s generated binding, $binding), and to set the @State in the custom Binding’s set method. You must also subscribe to iCloud change events and use those to update the value via the custom Binding.