Textfield in Foreach with EnvironmentObject - foreach

I'm trying to make two list of Textfield with a #EnvironmentObject but have "Use of unresolved identifier" problem
class ViewChange: ObservableObject {
#Published var Equipes: [Equipe] = EquipData
}
struct EquipView: View {
#EnvironmentObject var ViewChange: ViewChange
var body: some View {
ForEach(ViewChange.Equipes) { item in
Text("Équipe \(item.name)") //work
ForEach(item.joueurs){i in
Text(i.name) //work
TextField("", text: $i.name) // "Use of unresolved identifier '$i'"
}
}
}
struct Equipe : Identifiable {
var id = UUID()
var numero: Int
var name: String
var joueurs: Array<Joueur>
}
struct Joueur : Identifiable {
var id = UUID()
var name: String
}
let EquipData = [
Equipe(numero: 1, name: "Les Saiyans", joueurs: [Joueur(name: "Maximilien"),Joueur(name: "Paul")]),
Equipe(numero: 2, name: "Rocket", joueurs: [Joueur(name: "Roger"),Joueur(name: "Sacha")])
]
Someone can explain to me clearly how I can proceed to have my dynamic textField list with the values ​​of ViewChange.Equipes ?

Related

Binding with optional struct

I get the error "Value of optional type 'Photo?' must be unwrapped to refer to member 'name' of wrapped base type 'Photo'" when I try to send a optional struct on a binding of a TextField.
The content view code example:
import SwiftUI
struct ContentView: View {
#StateObject var viewModel = ContentViewViewModel()
private var photos = [
Photo(id: UUID(), name: "Exclamation", data: (UIImage(systemName: "exclamationmark.triangle")?.jpegData(compressionQuality: 1))!),
Photo(id: UUID(), name: "Circle", data: (UIImage(systemName: "circle.fill")?.jpegData(compressionQuality: 1))!)
]
var body: some View {
VStack {
DetailView(viewModel: viewModel)
}
.onAppear {
viewModel.selectedPhoto = photos[0]
}
}
}
The view model code example:
import Foundation
#MainActor
final class ContentViewViewModel: ObservableObject {
#Published var photos = [Photo]()
#Published var selectedPhoto: Photo?
}
The detail view code example (that uses the content view's view model):
import SwiftUI
struct DetailView: View {
#ObservedObject var viewModel: ContentViewViewModel
var body: some View {
TextField("Photo name here...", text: $viewModel.selectedPhoto.name)
}
}
Note that for some reasons I need the selectedPhoto property be optional.
You can create your own custom #Binding, so you can handle the process between getting data and set the updated here is an example:
If what you want is to select the photo in a List or Foreach you can bind directly to the array.
import SwiftUI
struct Photo {
let id: UUID
var name: String
let data: Data
}
#MainActor
final class ContentViewViewModel: ObservableObject {
#Published var photos = [Photo]()
#Published var selectedPhoto: Photo?
}
struct optionalBinding: View {
#StateObject var viewModel = ContentViewViewModel()
private var photos = [
Photo(id: UUID(), name: "Exclamation", data: (UIImage(systemName: "exclamationmark.triangle")?.jpegData(compressionQuality: 1))!),
Photo(id: UUID(), name: "Circle", data: (UIImage(systemName: "circle.fill")?.jpegData(compressionQuality: 1))!)
]
var body: some View {
VStack {
DetailView2(viewModel: viewModel)
Button("Select Photo") {
viewModel.selectedPhoto = photos[0]
}
}
}
}
struct DetailView2: View {
#ObservedObject var viewModel: ContentViewViewModel
var body: some View {
Text(viewModel.selectedPhoto?.name ?? "No photo selected")
TextField("Photo name here...", text: optionalBinding())
}
func optionalBinding() -> Binding<String> {
return Binding<String>(
get: {
guard let photo = viewModel.selectedPhoto else {
return ""
}
return photo.name
},
set: {
guard let _ = viewModel.selectedPhoto else {
return
}
viewModel.selectedPhoto?.name = $0
//Todo: also update the array
}
)
}
}

How to update attributes of a struct with TextFields made in ForEach

In SwiftUI I have a list of menu items that each hold a name, price etc. There are a bunch of categories and under each are a list of items.
struct ItemList: Identifiable, Codable {
var id: Int
var name: String
var picture: String
var list: [Item]
#State var newItemName: String
}
I was looking for a way to create a TextField inside each category that would add to its array of items.
Making the TextFields through a ForEach loop was simple enough, but I got stuck trying to add a new Item using the entered text to the right category.
ForEach(menu.indices) { i in
Section(header: Text(menu[i].name)) {
ForEach(menu[i].list) { item in
Text(item.name)
}
TextField("New Type:", text: /*some kind of bindable here?*/) {
menu[i].list.append(Item(name: /*the text entered above*/))
}
}
}
I considered using #Published and Observable Object like this other question, but I need the ItemList to be a Codable struct so I couldn't figure out how to fit the answers there to my case.
TextField("New Type:", text: menu[i].$newItemName)
Anyway any ideas would be appreciated, thanks!
You just have to focus your View.
import SwiftUI
struct ExpandingMenuView: View {
#State var menu: [ItemList] = [
ItemList(name: "Milk Tea", picture: "", list: [ItemModel(name: "Classic Milk Tea"), ItemModel(name: "Taro milk tea")]),
ItemList(name: "Tea", picture: "", list: [ItemModel(name: "Black Tea"), ItemModel(name: "Green tea")]),
ItemList(name: "Coffee", picture: "", list: [])
]
var body: some View {
List{
//This particular setup is for iOS15+
ForEach($menu) { $itemList in
ItemListView(itemList: $itemList)
}
}
}
}
struct ItemListView: View {
#Binding var itemList: ItemList
#State var newItemName: String = ""
var body: some View {
Section(header: Text(itemList.name)) {
ForEach(itemList.list) { item in
Text(item.name)
}
TextField("New Type:", text: $newItemName, onCommit: {
//When the user commits add to array and clear the new item variable
itemList.list.append(ItemModel(name: newItemName))
newItemName = ""
})
}
}
}
struct ItemList: Identifiable, Codable {
var id: UUID = UUID()
var name: String
var picture: String
var list: [ItemModel]
//#State is ONLY for SwiftUI Views
//#State var newItemName: String
}
struct ItemModel: Identifiable, Codable {
var id: UUID = UUID()
var name: String
}
struct ExpandingMenuView_Previews: PreviewProvider {
static var previews: some View {
ExpandingMenuView()
}
}
If you aren't using Xcode 13 and iOS 15+ there are many solutions in SO for Binding with array elements. Below is just one of them
ForEach(menu) { itemList in
let proxy = Binding(get: {itemList}, set: { new in
let idx = menu.firstIndex(where: {
$0.id == itemList.id
})!
menu[idx] = new
})
ItemListView(itemList: proxy)
}
Also note that using indices is considered unsafe. You can watch Demystifying SwiftUI from WWDC2021 for more details.
You can have an ObservableObject to be your data model, storing categories which then store the items.
You can then bind to these items, using Swift 5.5 syntax. This means we can write List($menu.categories) { $category in /* ... */ }. Then, when we write $category.newItem, we have a Binding<String> to the newItem property in Category.
Example:
struct ContentView: View {
#StateObject private var menu = Menu(categories: [
Category(name: "Milk Tea", items: [
Item(name: "Classic Milk Tea"),
Item(name: "Taro Milk Tea")
]),
Category(name: "Tea", items: [
Item(name: "Black Tea"),
Item(name: "Green Tea")
]),
Category(name: "Coffee", items: [
Item(name: "Black Coffee")
])
])
var body: some View {
List($menu.categories) { $category in
Section(header: Text(category.name)) {
ForEach(category.items) { item in
Text(item.name)
}
TextField("New item", text: $category.newItem, onCommit: {
guard !category.newItem.isEmpty else { return }
category.items.append(Item(name: category.newItem))
category.newItem = ""
})
}
}
}
}
class Menu: ObservableObject {
#Published var categories: [Category]
init(categories: [Category]) {
self.categories = categories
}
}
struct Category: Identifiable {
let id = UUID()
let name: String
var items: [Item]
var newItem = ""
}
struct Item: Identifiable {
let id = UUID()
let name: String
}
Result:

ForEach not working with Identifiable & id = UUID()

import SwiftUI
struct TestStudentView: View {
#StateObject var students = Students()
#State private var name = ""
#State private var numberOfSubjects = ""
#State private var subjects = [Subjects](repeating: Subjects(name: "", grade: ""), count: 10)
var body: some View {
NavigationView {
Group {
Form {
Section(header: Text("Student details")) {
TextField("Name", text: $name)
TextField("Number of subjects", text: $numberOfSubjects)
}
let count = Int(numberOfSubjects) ?? 0
Text("Count: \(count)")
Section(header: Text("Subject grades")) {
if count>0 && count<10 {
ForEach(0 ..< count, id: \.self) { number in
TextField("Subjects", text: $subjects[number].name)
TextField("Grade", text: $subjects[number].grade)
}
}
}
}
VStack {
ForEach(students.details) { student in
Text(student.name)
ForEach(student.subjects) { subject in //Does not work as expected
//ForEach(student.subjects, id:\.id) { subject in //Does not work as expected
//ForEach(student.subjects, id:\.self) { subject in //works fine with this
HStack {
Text("Subject: \(subject.name)")
Text("Grade: \(subject.grade)")
}
}
}
}
}
.navigationTitle("Student grades")
.navigationBarItems(trailing:
Button(action: {
let details = Details(name: name, subjects: subjects)
students.details.append(details)
}, label: {
Text("Save")
})
)
}
}
}
struct TestStudentView_Previews: PreviewProvider {
static var previews: some View {
TestStudentView()
}
}
class Students: ObservableObject {
#Published var details = [Details]()
}
struct Details: Identifiable {
let id = UUID()
var name: String
var subjects: [Subjects]
}
struct Subjects: Identifiable, Hashable {
let id = UUID()
var name: String
var grade: String
}
When I use - "ForEach(student.subjects, id:.id) { subject in" under normal circumstances it is supposed to work as id = UUID and the incorrect output is as follows:
then as the class conforms to Identifiable I tried - "ForEach(student.subjects) { subject in" it still does not work correctly. However, when I do - "ForEach(student.subjects, id:.self) { subject in" except I had to have the class conform to hashable and gives me the correct expected output. The correct output which is shown:
You need to use a map instead of repeating.
By using Array.init(repeating:) will invoke the Subjects to initialize only one time, and then insert that object into the array multiple times.
So all, in this case, all id is same.
You can check by just print all id in by this .onAppear() { print(subjects.map({ (sub) in print(sub.id) }))
struct TestStudentView: View {
#StateObject var students = Students()
#State private var name = ""
#State private var numberOfSubjects = ""
#State private var subjects: [Subjects] = (0...10).map { _ in
Subjects(name: "", grade: "")
} //<-- Here

ForEach TextField in SwiftUI

Let's say that I have a class Student
class Student: Identifiable, ObservableObject {
var id = UUID()
#Published var name = ""
}
Used within an Array in another class (called Class)
class Class: Identifiable, ObservableObject {
var id = UUID()
#Published var name = ""
var students = [Student()]
}
Which is defined like this in my View.
#ObservedObject var newClass = Class()
My question is: how can I create a TextField for each Student and bind it with the name property properly (without getting errors)?
ForEach(self.newClass.students) { student in
TextField("Name", text: student.name)
}
Right now, Xcode is throwing me this:
Cannot convert value of type 'TextField<Text>' to closure result type '_'
I've tried adding some $s before calling the variables, but it didn't seem to work.
Simply change the #Published into a #State for the Student's name property. #State is the one that gives you a Binding with the $ prefix.
import SwiftUI
class Student: Identifiable, ObservableObject {
var id = UUID()
#State var name = ""
}
class Class: Identifiable, ObservableObject {
var id = UUID()
#Published var name = ""
var students = [Student()]
}
struct ContentView: View {
#ObservedObject var newClass = Class()
var body: some View {
Form {
ForEach(self.newClass.students) { student in
TextField("Name", text: student.$name) // note the $name here
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
In general I'd also suggest to use structs instead of classes.
struct Student: Identifiable {
var id = UUID()
#State var name = ""
}
struct Class: Identifiable {
var id = UUID()
var name = ""
var students = [
Student(name: "Yo"),
Student(name: "Ya"),
]
}
struct ContentView: View {
#State private var newClass = Class()
var body: some View {
Form {
ForEach(self.newClass.students) { student in
TextField("Name", text: student.$name)
}
}
}
}

Cannot convert value of type 'NavigationLink<some View, EditView>' to closure result type '_'

I get the following error:
Cannot convert value of type 'NavigationLink' to
closure result type '_'
Do you know what's wrong here?
My ContentView file:
#ObservedObject var voucherData = VoucherData()
var body: some View {
NavigationView {
ZStack {
List {
ForEach(voucherData.voucherList) { voucher in
NavigationLink(destination: EditView(value:voucher.value, currency: voucher.currency, shopName: voucher.shopName)) {
VStack() {
And in an other file:
struct Voucher : Identifiable {
let id = UUID()
var value : String = ""
var currency : String = ""
var shopName : String = ""
}
final class VoucherData: ObservableObject {
#Published var voucherList: [Voucher] = [
.init(value: "100", currency: "USD", shopName: "FlyBurger")]
}
I assume your EditView is just missing the parameter voucher:
struct EditView: View {
let voucher: Voucher
...
}
Now you can pass the voucher like this:
NavigationLink(destination: EditView(voucher: voucher)) {

Resources