How to save settings selections

import SwiftUI

struct CardTheme: View {
  @State private var theme = globalVarTheme

  var body: some View {
  List {
  HStack {
  Text("Mono")
  .onTapGesture {
  self.setTheme(i: 0)
  }

  Spacer()

  if(theme == 0) {
  Image(systemName: "checkmark")
  .foregroundColor(Color.green)
  }

  }

  HStack {
  Text("Cool")
  .onTapGesture {
  self.setTheme(i: 1)
  }
  Spacer()

  if(theme == 1) {
  Image(systemName: "checkmark")
  .foregroundColor(Color.green)
  }
  }

  HStack {
  Text("Cute")
  .onTapGesture {
  self.setTheme(i: 2)
  }
  Spacer()

  if(theme == 2) {
  Image(systemName: "checkmark")
  .foregroundColor(Color.green)
  }
  }
  }
  .navigationBarTitle(Text(verbatim: "Card Order"))
  }

  func setTheme(i: Int) {
  theme = i

  }
}


I have a settings menu where the user picks a theme, the default value is set to a global variable, globalVarTheme, which is 0. But after they make a selection, exit that menu, and re-enter the menu it goes back to 0 (the first item) even if they have chosen one of the other items. How do I save their selection?

Also, what is the best way to save user selections beyond the current app session? Should I write all their selections to a plist file or is there a conventional way?

Replies

For a simple value like this, particularly a preference selected by the user, the default place to store it would be in the UserDefaults. It's possible (and indeed easy) to write a @UserDefaults property wrapper to handle this, but the issue then is getting it wired into SwiftUI state—your view updates only because you're using an @State variable, and you can't use more than one property wrapper, sadly.


The way I've tackled this sort of bifurcation before is using a little Combine magic. You can use a PassthroughSubject to feed values into as many different places as you'd like, including into an @State value or similar, but it takes a little setup work:


struct CardTheme: View {
    /// State variable used to update UI.
    @State private var theme: Theme = UserDefaults.standard.object(forKey: "Theme") as? Theme ?? defaultTheme

    /// Publisher used to change the value.
    private var themeSubject = PassthroughSubject<theme, never="">()
    
    /// Bucket holding on to the subscribers for the `themeSubject` publisher.
    private var subscribers: Set = []

    init() {
        // Attach something to write new values into UserDefaults.
        themeSubject
            .sink { UserDefaults.standard.set($0, forKey: "Theme") }
            .store(in: &subscribers)

        // Attach something to write new values into the @State variable.
        themeSubject
            .assign(to: \.theme, on: self)
            .store(in: &subscribers)
    }

    var body: some View {
        Button("Touch me") {
            // sends a new value through the publisher into UserDefaults and State.
            self.themeSubject.send(Theme.someValue)
        }
    }
}


An alternative approach with less boilerplate code to write would be to use @ObservedObject and @Published. For this to work, your preference would need to be in a class type, not a struct. Basically you'd create an object like this:


class GlobalPrefs: ObservableObject {
    static let shared = GlobalPrefs()

    @Published var theme: Theme = UserDefaults.standard.object(forKey: "Theme") as? Theme ?? defaultTheme

    /// Users must use `GlobalPrefs.shared` to get the singleton instance.
    private init() {}
}

struct CardTheme: View {
    @ObservedObject var prefs = GlobalPrefs.shared
    private var defaultsSubscriber: AnyCancellable? = nil

    init() {
        /// The $-prefix on a @Published property returns a Publisher to which you can subscribe.
        defaultsSubscriber = prefs.$prefs.sink {
            UserDefaults.standard.set($0, forKey: "Theme")
        }
    }
}


Using @ObservedObject in this way lets SwiftUI know when you access its contents, similar to the way @State works. Thus, in your code, you just do something like this and it'll all work:


if prefs.theme == .someValue {
    // do something
}

Button("Set Theme X") {
    self.prefs.theme = Theme.someValue
}

One extra thing: rather than use Text("Something").onTapGesture(), I'd suggest a Button. You can set the foreground color explicitly to avoid it turning blue:


Button("Cool") {
    self.setTheme(i: 1)
}
.foregroundColor(.primary)
import SwiftUI

struct CardTheme: View {
    //@State private var theme = 0
    @State private var theme = UserDefaults.standard.integer(forKey: "Card Theme")
    
      var body: some View {
        List {
          HStack {
            Text("Mono")
              //.font(.system(size: 12))
              .onTapGesture {
                self.setTheme(i: 0)
            }
            
            Spacer()
            
            if(theme == 0) {
              Image(systemName: "checkmark")
                .foregroundColor(Color.green)
            }
            
          }
          
          HStack {
            Text("Cool")
             // .font(.system(size: 12))
              .onTapGesture {
                self.setTheme(i: 1)
            }
            Spacer()
            
            if(theme == 1) {
              Image(systemName: "checkmark")
                .foregroundColor(Color.green)
            }
          }
          
          HStack {
            Text("Cute")
             // .font(.system(size: 12))
              .onTapGesture {
                self.setTheme(i: 2)
            }
            Spacer()
            
            if(theme == 2) {
              Image(systemName: "checkmark")
                .foregroundColor(Color.green)
            }
          }
        }
        .navigationBarTitle(Text(verbatim: "Card Theme"))
      }
    
    func setTheme(i: Int) {
      theme = i
      UserDefaults.standard.set(i, forKey: "Card Theme")
    }
}


Sorry I'm having trouble following your code. But if I modified mine to use UserDefaults, i can see that it is correctly saving and retrieving the selection as I can close the app and re-open it and have the correct theme row ticked. The only issue is when I go back to the previous view and re-enter the Card Theme view, it is still showing the same selection as from when the app loaded, rather than any changes in selection since the app loaded. Is there a quick fix for that? For example can I incorporate @EnvironmentObject? Thanks.