swiftmacosswiftuiappstorage

Using @AppStorage from within a view that has multiple instances (ie. each view has a different setting in it)


I've racked my brains out with this and ChatGPT is in the endless error loop it sometimes does.

I have a view called OptionSelector which is a visual selector of a bunch of options where it's called like this:

OptionSelector(title: "Difficulty:", options: ["Easy", "Medium", "Hard"])
OptionSelector(title: "Map Layout:", options: ["Normal", "Crazy", "Asylum"])

Etc.

I am using this to set different options. but they are all ONE view that's defined in OptionSelector.swift:

struct OptionSelector: View { ... }

The state variable is an Int that stores the index of the array. Clicking on it advances it by one and at the end loops back to the first option. This is the way it has to be. I don't want it to be any other way.

So you see that if I do:

@AppStorage("activeOption") var activeOption: Int = 0

Then when I run the app, clicking on one toggles through ALL the option selectors in the app, and then when I run it again, it crashes because it saved one that had more options than some others and it gets an index out of bounds error.

Now ChatGPT told me to add an optionKey to the call, so I would add optionKey: "selectedMap" for example.

Unfortunately when I do:

@AppStorage(optionKey) var activeOption = 0

it gives me an error saying I can't use optionKey inside there because it won't exist at the time that it's run.

Apparently no one on the Internet is abstracting things like I am and making reusable views for the same exact type of things like I'm doing..

Coz I would think this would be "AppStorage 101" but apparently no one is even trying to do this. All I find are using it to do things like App-wide dark mode etc.

but nothing using abstract reusable components/views/etc.

So I am posting this here in hopes someone can give me the answer and will remove the saving functionality until I get an answer that works.

Thanks!


Solution

  • I appreciate Sweeper's answer, though somehow for some reason it becomes impossible if I abstract my code further to have the list of options in a configOptions array and @AppStorage ended up being more trouble than it's worth, and never seemed to be without an exponential amount of errors being added after every attempt to fix it.

    I decided to completely scrap it in favor of UserDefaults and it was infinitely easier to use.

    Using the code in Sweeper's answer, with some modifications to reflect the actual OptionSelector, I will show my solution:

    let defaults = UserDefaults.standard
    
    let defaultDifficulty: Int = 0
    let defaultMapLayout: Int = 1
    
    var difficulty: Int = (defaults.object(forKey: "difficulty") != nil) ? defaults.integer(forKey: "difficulty") : defaultDifficulty
    var mapLayout: Int = (defaults.object(forKey: "mapLayout") != nil) ? defaults.integer(forKey: "mapLayout") : defaultMapLayout
    
    struct GameOption {
        let key: String
        let title: String
        let options: [String]
        var selection: Int
    }
    
    
    var configOptions: [GameOption] = [
        GameOption(key: "difficulty", title: "Difficulty", options: ["Easy", "Medium", "Hard"], selection: difficulty),
        GameOption(key: "mapLayout", title: "Map Layout", options: ["Normal", "Crazy", "Asylum"], selection: mapLayout)
    ]
    
    
    struct OptionSelector: View {
        let key: String
        let title: String
        let options: [String]
        @State var selection: Int
    
        var body: some View {
            VStack(spacing: 10) {
                Text(title.uppercased())
                    .background(.black)
                    .foregroundColor(.white)
                    .font(.custom("Apple Symbols", size: 18))
                Text(options[selection].uppercased())
                    .frame(width: 320, alignment: .center)
                    .background(.black)
                    .foregroundColor(.white)
            }
                .gesture(
                    TapGesture()
                        .onEnded {
                            selection = (selection + 1) % options.count
                            defaults.set(selection, forKey: key)
                        }
                )
        }
    }
    
    // Reuse it like this:
    struct ContentView: View {
        var body: some View {
            VStack(spacing: 20) {
                ForEach(configOptions, id: \.title) { option in
                    OptionSelector(
                        key: option.key,
                        title: option.title,
                        options: option.options,
                        selection: option.selection
                    )
                }
            }
            .background(Color.black)
        }
    }
    

    This code quite simply keeps all the apps settings in UserDefaults and they persist properly when the app is quit and restarted.

    For the initial scope I had of settings surviving quitting the app and restarting it (to save app state) this works perfectly.

    I have since added a reset option to reset to defaults and while it does reset everything and upon next app restart it shows all default settings, it does not immediately show the changes in the app.

    I think I know why but that's outside the scope of this question.

    I would recommend anyone else who is having problems with @AppStorage to use UserDefaults instead. It sounds at first like @AppStorage is easier coz it's a wrapper and the alleged way of using it is simple, but I've run into nothing but problems when trying to implement it when the code isn't extremely simple.

    In contrast using UserDefaults on its own is infinitely simpler than the "wrapper" that's supposed to make it simpler.

    Eventually I'd like to have the configOptions set up in a JSON file that is user-configurable once I expand the app, which is why I am doing it the way I'm doing it.

    It's just for now I wanted to keep it simple-- Implementing JSON was a bit too much right now when I don't really need it yet.

    The source I used to learn how to use UserDefaults without @AppStorage:

    https://www.hackingwithswift.com/read/12/2/reading-and-writing-basics-userdefaults