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:
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")
}
A toggle in SwiftUI is a control which can go between on and off states and it’s equivalent to UISwitch in UIKit. In this tutorial we’re going to learn how to create a toggle and use it in various examples.
Saving Date/Time to UserDefaults through didSet on @Published var (from TimePicker component of SwiftUI) 0 Pass an @State variable to ContentView
First, create a single view iOS app using SwiftUI. Then, create a new Swift File in your Xcode project and call it UserSettings.swift. Inside the new file, implement a class called UserSettings, conforming to the ObservableObject, with one @Published String variable holding username from the UI form.
I read your 2019 comment as advising to use UIKit instead of SwiftUI for this project, since SmushyTaco hadn't yet learned UserDefaults. As an aside, though, there are aspects of UserDefaults (like how it works with Combine) which work very well in SwiftUI but have nearly zero examples in UIKit
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)
}
}
}
With help both from this video by azamsharp and this tutorial by Paul Hudson, I've been able to produce a toggle that binds to UserDefaults and shows whichever change you've assigned to it instantaneously.
Add this line of code under 'window' variable
var settingsStore = SettingsStore()
And modify window.rootViewController to show this
window.rootViewController = UIHostingController(rootView: contentView.environmentObject(settingsStore))
import Foundation
class SettingsStore: ObservableObject {
@Published var isOn: Bool = UserDefaults.standard.bool(forKey: "isOn") {
didSet {
UserDefaults.standard.set(self.isOn, forKey: "isOn")
}
}
}
If so you wish, create a SwiftUI View called this and paste:
import SwiftUI
struct SettingsStoreMenu: View {
@ObservedObject var settingsStore: SettingsStore
var body: some View {
Toggle(isOn: self.$settingsStore.isOn) {
Text("")
}
}
}
Don't forget to inject SettingsStore to SettingsStoreMenu from whichever Main View you have, such as
import SwiftUI
struct MainView: View {
@EnvironmentObject var settingsStore: SettingsStore
@State var showingSettingsStoreMenu: Bool = false
var body: some View {
HStack {
Button("Go to Settings Store Menu") {
self.showingSettingsStoreMenu.toggle()
}
.sheet(isPresented: self.$showingSettingsStoreMenu) {
SettingsStoreMenu(settingsStore: self.settingsStore)
}
}
}
}
(Or whichever other way you desire.)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With