SwiftUI, AppStorage and using Picker in TabViews

530 views Asked by At

I am trying to save a small amount of data with picker using AppStorage across multiple views. However the issue I'm running into is that when I select one value and link to AppStorage it changes the value for all the others. What I want is to save the value for each selection over multiple views. If I use @State variable the selections work fine, but the values don't get saved when I close and reopen the app. I'm pretty sure I need to send each selection to it's own @AppStorage variable, but I'm having a hard time figuring out how to do that.

struct Animals: Identifiable {
    var id = UUID().uuidString
    var name: String
    var animalTypes: [AnimalTypes]
}
var allAnimals = [
    Animals(name: "fred", animalTypes: [.shiba, .lab]),
    Animals(name: "barney", animalTypes: [.shiba, .dobberman, .lab]),
    Animals(name: "molly", animalTypes: [.snowshoe, .burmese, .bengal]),
    Animals(name: "bob", animalTypes: [.burmese]),
    Animals(name: "wilma", animalTypes: [.snowshoe, .bengal]),
]

enum AnimalTypes: String, CaseIterable, Codable {
    // Dog Breeds
    case shiba, lab, dobberman
    // Cat Breeds
    case snowshoe, burmese, bengal
}

struct AnimalsView: View {

    
    @State var animals: Animals!
    
    var body: some View {
        TabView {
            ForEach(allAnimals) { animal in
                AnimalSelectionView(animals: animal)
                .tag(animal.name)
            }
        }
        .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
    }
}

struct AnimalSelectionView: View {
    
    @AppStorage("selection") var animalSelection: Int = 0 // Saves the same value across all pickers (how to save individual values?)
//    @State private var animalSelection: Int = 0 // Sets value for each picker in each tabview, but doesn't save
    @State var animals: Animals!
    
    var body: some View {
        VStack {
            if animals.animalTypes.count <= 1 {
                Text("\(animals.animalTypes[0].rawValue)")
            } else {
                Text("\(animals.animalTypes[animalSelection].rawValue)")
            }
            
            if animals.animalTypes.count > 1 {
                Picker(selection: $animalSelection, label: Text("Select Animal Type")) {
                    ForEach(0 ..< animals.animalTypes.count, id:\.self) { item in
                        Text("\(item + 1)")
                            .font(.caption)
                    }
                }
                .pickerStyle(SegmentedPickerStyle())
                .frame(width: 100, height: 17)
            }
        }
    }
}
1

There are 1 answers

3
Carter Foughty On BEST ANSWER

I see that you have decided to create a property on your Animals class called id. Happily, this is the perfect thing to use to save unique UserDefaults values for each Animals object.

You can do something like this:

struct AnimalSelectionView: View {

    @AppStorage var animalSelection: Int
    @State var animals: Animals

    init(animals: Animals) {
        self.animals = animals
        // The below line simply assigns the key which should be used to access your
        // desired variable. This way, it can be keyed specifically to your `Animals`
        // id value.
        self._animalSelection = AppStorage(wrappedValue: 0, "selection-\(animals.id)")
    }

    ...

}

Keep in mind that you won't be able to retrieve the same value if you instantiate two different objects that have the same arguments passed in the init.

Example:

let fred = Animals(name: "fred", animalTypes: [.shiba, .lab])
let fred2 = Animals(name: "fred", animalTypes: [.shiba, .lab])

Edit for clarity:

Here, fred and fred2 are mostly the same, but the id property for each of these instances of Animals will be different. Therefore, if you try to access the selection value of one using the other's id, you will not receive the correct value. You must hold on to the exact Animals instance to access the value that you stored in UserDefaults, or at least hold on to its id as needed. This rule holds true across app sessions. If you instantiate an Animals object and then quit and reboot the app, when that same Animals object is re-instantiated, it will be assigned a different id value as per your definition Animals.id = UUID().uuidString.

In order to persist their ids across app sessions, you can either store the animals in CoreData, UserDefaults, grant them a static id, or generate an id based on the Animals properties.

Creating a static id would look like this:

`Animals(id: "animal-fred", name: "fred", animalTypes: [.shiba, .lab])`

Generating an id based on their properties might look like:

Animals: Identifiable {
    var id: {
        // We concatenate the name with each of the `AnimalsTypes`
        // by mapping the `AnimalsTypes` to their relevant strings
        // and joining them by a comma.
        // The resulting `id` might look like:
        // "fred-shiba,lab"
        return name + "-" + animalTypes.map { $0.rawValue }.joined(separator: ",")
    }
    var name: String
    var animalTypes: [AnimalTypes]
}

The method of generating ids based on the Animals properties works well, although if you create two Animals with the exact same properties, your logic won't be able to tell them apart as they will generate the same id. They will then fetch and set the same value in UserDefaults because they would share the same key.

If you intend for these animals to be created dynamically by the users of your app, you will need to store them either in CoreData or UserDefaults. If, however, your animals will all be created by you, you can either statically define the ID or generate them based on the Animals properties.