UserDefaults Binding with Toggle in SwiftUI

gohnjanotis picture gohnjanotis · Jun 30, 2019 · Viewed 7k times · Source

I'm trying to figure out the best way to build a simple settings screen bound to UserDefaults.

Basically, I have a Toggle and I want:

  • the value a UserDefault to be saved any time this Toggle is changed (the UserDefault should be the source of truth)
  • the Toggle to always show the value of the UserDefault

Settings screen with Toggle

I have watched many of the SwiftUI WWDC sessions, but I'm still not sure exactly how I should set everything up with the different tools that are available within Combine and SwiftUI. My current thinking is that I should be using a BindableObject so I can use hat to encapsulate a number of different settings.

I think I am close, because it almost works as expected, but the behavior is inconsistent.

When I build and run this on a device, I open it and turn on the Toggle, then if I scroll the view up and down a little the switch toggles back off (as if it's not actually saving the value in UserDefaults).

However, if I turn on the switch, leave the app, and then come back later it is still on, like it remembered the setting.

Any suggestions? I'm posting this in hopes it will help other people who are new to SwiftUI and Combine, as I couldn't find any similar questions around this topic.

import SwiftUI
import Combine

struct ContentView : View {

    @ObjectBinding var settingsStore = SettingsStore()

    var body: some View {
        NavigationView {
            Form {
                Toggle(isOn: $settingsStore.settingActivated) {
                    Text("Setting Activated")
                }
            }
        }.navigationBarTitle(Text("Settings"))
    }
}

class SettingsStore: BindableObject {

    var didChange = NotificationCenter.default.publisher(for: .settingsUpdated).receive(on: RunLoop.main)

    var settingActivated: Bool {
        get {
            UserDefaults.settingActivated
        }
        set {
            UserDefaults.settingActivated = newValue
        }
    }
}

extension UserDefaults {

    private static var defaults: UserDefaults? {
        return UserDefaults.standard
    }

    private struct Keys {
        static let settingActivated = "SettingActivated"
    }

    static var settingActivated: Bool {
        get {
            return defaults?.value(forKey: Keys.settingActivated) as? Bool ?? false
        }
        set {
            defaults?.setValue(newValue, forKey: Keys.settingActivated)
        }
    }
}

extension Notification.Name {
    public static let settingsUpdated = Notification.Name("SettingsUpdated")
}

Answer

atulkhatri picture atulkhatri · Dec 6, 2019

Update

------- iOS 14: -------

Starting iOS 14, there is now a very very simple way to read and write to UserDefaults.

Using a new property wrapper called @AppStorage

Here is how it could be used:

import SwiftUI

struct ContentView : View {

    @AppStorage("settingActivated") var settingActivated = false

    var body: some View {
        NavigationView {
            Form {
                Toggle(isOn: $settingActivated) {
                    Text("Setting Activated")
                }
            }.navigationBarTitle(Text("Settings"))
        }
    }
}

That's it! It is so easy and really straight forward. All your information is being saved and read from UserDefaults.

-------- iOS 13: ---------

A lot has changed in Swift 5.1. BindableObject has been completely deprecated. Also, there has been significant changes in PassthroughSubject.

For anyone wondering to get this to work, below is the working example for the same. I have reused the code of 'gohnjanotis' to make it simple.

import SwiftUI
import Combine

struct ContentView : View {

    @ObservedObject var settingsStore: SettingsStore

    var body: some View {
        NavigationView {
            Form {
                Toggle(isOn: $settingsStore.settingActivated) {
                    Text("Setting Activated")
                }
            }.navigationBarTitle(Text("Settings"))
        }
    }
}

class SettingsStore: ObservableObject {

    let willChange = PassthroughSubject<Void, Never>()

    var settingActivated: Bool = UserDefaults.settingActivated {
        willSet {

            UserDefaults.settingActivated = newValue

            willChange.send()
        }
    }
}

extension UserDefaults {

    private struct Keys {
        static let settingActivated = "SettingActivated"
    }

    static var settingActivated: Bool {
        get {
            return UserDefaults.standard.bool(forKey: Keys.settingActivated)
        }
        set {
            UserDefaults.standard.set(newValue, forKey: Keys.settingActivated)
        }
    }
}