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)
}
}
}
}
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 id
s 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 id
s 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.