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 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.
Related
Let's say I have 2 entities:
GameSession :which has Attributes "date", "place", "numberofplayer" + a relationship called "players" with "Player"
Player: which has Attributes "name","score_part1","score_part2","score_part3" + a relationship with "GameSession"
the relationship is "one to many": One session can have many players
Let's say now I have a list of GameSession and when I click on on one (with a NavigationLink)
It sends me to a new view where I can see:
All the names of the players of that session (in text) and also right next to the player name I would like to have 3 TextField in which I can enter (an update) "score_part1","score_part2","score_part3" for every players of that session
Basically I am able to display the name of all the players of a given session, But it seems impossible to have the "score_part1","score_part2","score_part3" in editable TextField...
I have an error saying "Cannot convert value of type 'String' to expected argument type 'Binding<String>'"
Basically in my first view I have something like that:
struct RamiListePartieUIView: View {#Environment(.managedObjectContext) var moc#FetchRequest(entity: GameSession.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \GameSession.date, ascending: false)]) var gamesessions: FetchedResults<GameSession>
var body: some View {
VStack {
List {
ForEach(gamesessions, id: \.date) { session in
NavigationLink (destination: DetailPartieSelecUIView(session: session)){
Text("\(session.wrappedPlace) - le \(session.wrappedDate, formatter: itemFormatter) ")
}
}
.onDelete(perform: deleteSessions)
.padding()
}
}
}
}
And in my second view I have something like that:
struct DetailPartieSelecUIView: View {
#State var session:GameSession
#Environment(\.managedObjectContext) var moc
var body: some View {
Section("Ma session du \(session.wrappedDate, formatter: itemFormatter)"){
ForEach(session.playersArray, id: \.self) { player in
HStack {
Text(player.wrappedName) // OK it works
TextField("score", text : player.wrappedScore_part1) // it generates an error
TextField("score", text : player.wrappedScore_part2) // it generates an error
TextField("score", text : player.wrappedScore_part3) // it generates an error
}
}
}
}
}
private let itemFormatter: DateFormatter = {
let formatter = DateFormatter()
// formatter.dateStyle = .short
// formatter.timeStyle = .medium
formatter.dateFormat = "YYYY/MM/dd" //"YY/MM/dd"
return formatter
}()
also,
I have defined the "wrappedScore_part1","wrappedScore_part2","wrappedScore_part3" in the Player+CoreDataProperties.swift file
and "wrappedPlace", "wrappedData" as well as the "PlayersArray" in the GameSession+CoreDataProperties.swift file
it is done like that:
public var wrappedPlace: String {
place ?? "Unknown"
}
// Convert NSSet into an array of "Player" object
public var playersArray: [Player] {
let playersSet = players as? Set<Player> ?? []
return playersSet.sorted {
$0.wrappedName< $1.wrappedName
}
}
I am new at coding with swiftUI so I am probably doing something wrong... If anyone can help me it would be much appreciated.
Thanks a lot
I have tried a lot of things. Like changing the type of my attribute to Int32 instead os String. As I am suppose to enter numbers in those fields, I thought it would be best to have Integer. But it didn't change anything. and ultimately I had the same kind of error message
I tried also to add the $ symbol, like that:
TextField("score", text : player.$wrappedScore_part1)
But then I had other error message popping up at the row of my "ForEach", saying "Cannot convert value of type '[Player]' to expected argument type 'Binding'"
And also on the line just after the HStack, I had an error saying "Initializer 'init(_:)' requires that 'Binding' conform to 'StringProtocol'"
Thank you for your help!
Best regards,
JB
Your first problem of how to fetch the players in a session you need to supply a predicate to the #FetchRequest<Player>, e.g.
#FetchRequest
private var players: FetchedResults<Player>
init(session: Session) {
let predicate = NSPredicate(format: "session = %#", session)
let sortDescriptors = [SortDescriptor(\Player.timestamp)] // need something to sort by.
_players = FetchRequest(sortDescriptors: sortDescriptors, predicate: predicate)
}
That acts like a filter and will only return the players that have the session relation equalling that object. The reason you have to fetch like this is so any changes will be detected.
The second problem about the bindings can be solved like this:
struct PlayerView: View{
#ObservedObject var player: Player {
var body:some View {
if let score = Binding($player.score) {
TextField("Score", score)
}else{
Text("Player score missing")
}
}
}
This View takes the player object as an ObservedObject so body will be called when any of its properties change and allows you to get a binding to property. The Binding init takes an optional binding and returns a non-optional, allowing you to use it with a TextField.
I' trying to create a set of Toggles, that need to be stored in one core data field of type "Transformable". I started with this example:
https://developer.apple.com/forums/thread/118595
in combination with other ideas from stack.
I'm trying to get this way:
Create a Set of structs like this
struct AllScopes: Identifiable, Hashable {
var id: UUID
var name: String
var notify: Bool
}
[...]
// all the stuff with View and body with
#State var scopes = Set<AllScopes>()
[...]
// and here I run through my FetchRequest to fill the Set
.onAppear {
for scope in allScopes {
scopes.insert(
AllScopes(
id: scope.id!,
name: scope.name!,
notify: false
)
)
}
}
In the end I've got a nice Set with all my scopes.
I call a new View with YearlyReportPage6(scopes: $scopes)
And now my problem - the next view:
struct YearlyReportPage6: View {
#Binding var scopes: Set<AllScopes>
init(scopes: Binding<Set<AllScopes>>) {
_scopes = scopes
}
var body: some View {
VStack {
ForEach(scopes.indices) { index in
Toggle(isOn: self.$scopes[index].notify) {
Text(self.scopes[index].name)
}
}
}
}
}
But all in ForEach creates errors. Either Binding in isOn: is wrong, or ForEach can't work with the set, or the Text is not a String, or, or, or...
In the end there should be a list of Toggles (checkboxes) and the selection should be stored in database.
Changing the Set into a simple Array like #State var scopes = [AllScopes]() will work for the Toggles, but how can I store this into a Transformable?
ForEach(Array(scopes).indices) { index in
Toggle(isOn: self.$scopes[index].notify) {
Text(self.scopes[index].name)
}
}
To summarize:
either how can I create the list of Toggles with the Set of AllScopes
or how can I store the Array / Dictionary into the Transformable field?
I hope, you can understand my clumsy English. :-)
In my model data class I have a task array.
class Model: ObservableObject {
#Published var tasks: [Task] = Task.sampleData
}
In my HomeView I display these tasks however the user has the ability to use a picker to sort them by the date they were created or by the date they are due. In the HomeView I have an enum that contains the options that the user can sort them by and a state instance to hold the picked value. By default it is set to sort by due date.
#EnvironmentObject private var model: Model
#State private var sortOption: SortOption = .dueDate
enum SortOption {
case dueDate, creationDate
}
I decided to then create a computed value that contains the sorted array so I don't change the original tasks array as it is used to display tasks in other parts of the app.
var sortedTasks: [Task] {
model.tasks.sorted(by: {
if sortOption == .dueDate {
return $0.dueDate < $1.dueDate
} else {
return $0.creationDate < $1.creationDate
}
})
}
And soon after I ran into the problem. My Task Row requires a binding task and I am unable to retrieve the required binding from the computed property which was the sortedTasks array.
// ERROR: Cannot find '$sortedTasks' in scope
ForEach($sortedTasks) { $task in
TaskRow(task: $task)
}
My understanding is that if I want to keep the sortedTasks array then I will have to remove the binding property from the TaskRow but if I want to keep the binding property what should the approach be to display a sorted array without manipulating the tasks array in the data model class? Could the computed property return a sorted array of binding task objects?
The sort depends on sortOption which is view state so it is not correct to do it in the model because then you can't have different views using different sort options. The solution is to sort the bindings, not the tasks, e.g.
var sortedTasks: [Binding<Task>] {
$model.tasks.sorted { $x, $y in // $model.tasks.animation().sorted if you would like animations like List row moves.
if sortOption == .dueDate {
return x.dueDate < y.dueDate
} else {
return x.creationDate < y.creationDate
}
}
}
ForEach(sortedTasks) { $task in
TaskRow(task: $task)
}
You might be wondering how ForEach can find the id, well that's because Binding has #dynamicMemberLookup which means it forwards property lookups to its value.
You can´t do that but you can use a Combine approach here:
Move your sortedTasks, sortOption var to your class named Model and create a combined publisher from your two vars that should trigger the sortedTasks var to update. Sort the received collection and assign it to your sortedTasks.
class Model: ObservableObject {
#Published var tasks: [TaskModel] = TaskModel.sampleData
#Published var sortedTasks: [TaskModel] = TaskModel.sampleData
#Published var sortOption: SortOption = .dueDate
init(){
Publishers.CombineLatest($sortOption, $tasks)
.map{ sortOption, tasks in
// apply sorting here
}.assign(to: &$sortedTasks) // assign to your sorted var
}
}
Use it like:
var body: some View{
HStack{
ForEach($model.sortedTasks) { $task in
// TaskRow(task: $task)
}
}
}
I´ve renamed the Task model to TaskModel as i think it is a very bad idea to have a struct named after a system provided class.
I have a parent view of AttackParameters which is calling a child view (MissileView)... I am sending MissileView three things. 2 arrays, which give the Missile view enough to create a grid based on those two arrays and the grid is a grid of text inputs, like Soduko. Then I am passing in an array of names so that is has the same number of rows and cols as the grid.
ForEach(missileSelections, id:\.self) { missile in
if showGreeting{
Text(missile).padding()
MissileView(launchPointCount: launchPointsSelections, targetCount: targetSelections, names: [[String]](repeating: [String](repeating: "", count: targetSelections.count), count: launchPointsSelections.count)).padding(.horizontal, 20)
}
}
}
As you can see above I am calling the MissileView there.
GridStack(rows: launchPointCount.count, columns: targetCount.count) { row, col in
TextField("", text: $names[row][col]).border(Color.black, width: 0.5).background(Color.white).keyboardType(.decimalPad).onChange(of: names)
Above is the missileView class. It has a 2d array of text inputs, but I would like to after the user puts text in the grid of text inputs to return that array back to the parent view. How can I do this?
to do that you need something like this:
struct MissileView: View {
#Binding var launchPointCount: [String]
#Binding var targetCount: [String]
#Binding var names: [[String]]
...
}
with
struct AttackParametersView: View {
....
#State var names = [[String]]()
...
MissileView(launchPointCount: $launchPointsSelections,
targetCount: $targetSelections,
names: $names))
}
and somewhere before calling "MissileView(....)" do:
names = [[String]](repeating: [String](repeating: "",
count: targetSelections.count),
count: launchPointsSelections.count)
or put this in MissileView, something like:
.onAppear(perform: {
names = [[String]](repeating: [String](repeating: "", count: targetCount.count), count: launchPointCount.count)
})
Edit:
I cannot do all your code for you, however your setup is complicated enough
that you should look into creating a dedicated model
that holds the state of your system, and use that for the inputs you want for each "grid".
An ObservableObject model that you pass around, for example:
class AttackParameters: ObservableObject {
#Published var grids ....
#Published var names ....
...
}
then use it in AttackParametersView, and similarly in MissileView:
#ObservedObject var attackParameters = AttackParameters()
So when your attackParameters model changes, all views will know about it and adjust accordingly.
Okay so I've been working on this for several days now and have not had any luck with an answer that makes any sense. I have a form in SwiftUI, using #ObservedObject to pull variables from a struct. In that form, I have a variety of text fields and pickers that the user can interact with. HOWEVER, I cannot figure out how to get my "Add" button to actually add that data to any of the other views in the app. I followed the sandwiches tutorial from WWDC20, with significant changes, so there is a swift file with "testData" and essentially I'm trying to get it so that the button uses the user input to append the testData and show that instead of nothing.
struct Horse: Identifiable {
var id = UUID()
var name: String
var gender: String
var breed: String
var type: String
var scale: String
var brand: String
var finish: String
var specialty: String
var imageName: String { return name }
var thumbnailName: String { return name + "Thumb" }
}
let testData = [
Horse(name: "Van Gogh", gender: "Stallion", breed: "Unknown", type: "Customized", scale: "Stablemate", brand: "Peter Stone", finish: "Gloss", specialty: "OOAK")
]
So this is what I'm using to establish testData and the parameters for what should be included in it.
func addANewHorse() {
withAnimation {
testStore.horses.append(Horse(name: "\(horseDetails.horseName)", gender: "\(horseDetails.selectedGender.rawValue)", breed: "\(horseDetails.horseBreed)", type: "\(horseDetails.type.rawValue)", scale: "\(horseDetails.scale.rawValue)", brand: "\(horseDetails.brand.rawValue)", finish: "\(horseDetails.finish.rawValue)", specialty: "\(horseDetails.specialty.rawValue)"))
}
}
Button("Add", action: {
addANewHorse();
self.presentationMode.wrappedValue.dismiss()
})
And that is what I'm using to try and append the testData to update with the users input. I know this is kind of choppy but does anyone have any advice whatsoever?
---EDIT---
My main app file looks like this...
#main
struct Pferd_HerdApp: App {
#StateObject private var store = HorseStore()
#StateObject private var horseDetails = HorseDetails()
var body: some Scene {
WindowGroup {
ContentView(store: store, horseDetails: HorseDetails())
}
}
}
my horse store class looks like this...
class HorseStore: ObservableObject {
#Published var horses: [Horse]
init(horses: [Horse] = []) {
self.horses = horses
}
}
let testStore = HorseStore(horses: testData)
Also, "HorseDetails" is the observableobject I'm trying to pull data from to append the testData, so here is the code for that
class HorseDetails: ObservableObject {
#Published var horseName = ""
#Published var selectedGender = Gender.allCases[0]
#Published var horseBreed = ""
#Published var purchaseDate = Date()
#Published var winCount = ""
#Published var notes = ""
#Published var brand = Brands.allCases[0]
#Published var type = Type.allCases[0]
#Published var scale = Scale.allCases[0]
#Published var finish = Finish.allCases[0]
#Published var specialRun = false
#Published var specialty = Specialty.allCases[0]
}
var horseDetails = HorseDetails()
and I changed the let for testData to a variable
Since your Question leaves a lot of code out, I will be making a few assumptions. I'm assuming that your form (where you have the button to add data) and your view for displaying the data are in two different views. You have not included your view model in the code, although there was an instance of your view model (testStore) used in the code above. You need to make sure that somewhere at the root of your view hierarchy, you made an instance of your view model (I'm assuming its called TestStoreViewModel) and passed that as an environment object to your subviews. For example, you should something like this
#main
struct YourApp: App {
let testStoreViewModel = TestStoreViewModel()
var body: some Scene {
WindowGroup {
ContentView().environmentObject(testStoreViewModel)
}
}
}
in all of your views where you need to use the data from your TestStoreViewModel, you should declare it like so
#EnvironmentObject var testStore:TestStoreViewModel
Using environment objects means that your observable object is automatically synced across all of your views that use the environment object. Everything else in the code above should work fine with the use of EnvironmentObjects and a single source of truth. For more on environment objects, you can check out this article which in my opinion is great at explaining Environment Objects in swiftui. It is also important to note that in that article, it mentioned the use of a SceneDelegte and the ContentView being wrapped around a UIHostingController. That was replaced by the first block of code I showed you above.