Swift Array .append() method not working in SwiftUI - ios

I'm struggling to do a simple append in SwiftUI. Here's my code:
// This is defined in my custom view
var newClass = Class()
// This is inside a List container (I hid the Button's content because it doesn't matter)
Button(action: {
self.newClass.students.append(Student())
print(self.newClass.students) // This prints an Array with only one Student() instance - the one defined in the struct's init
})
// These are the custom structs used
struct Class: Identifiable {
var id = UUID()
#State var name = ""
#State var students: [Student] = [Student()] // Right here
}
struct Student: Identifiable {
var id = UUID()
#State var name: String = ""
}
I think it might be somehow related to the new #Struct thing, but I'm new to iOS (and Swift) development, so I'm not sure.

Let's modify model a bit...
struct Class: Identifiable {
var id = UUID()
var name = ""
var students: [Student] = [Student()]
}
struct Student: Identifiable {
var id = UUID()
var name: String = ""
}
... and instead of using #State in not intended place (because it is designed to be inside View, instead of model), let's introduce View Model layer as
class ClassViewModel: ObservableObject {
#Published var newClass = Class()
}
and now we can declare related view that behaves as expected
struct ClassView: View {
#ObservedObject var vm = ClassViewModel()
var body: some View {
Button("Add Student") {
self.vm.newClass.students.append(Student())
print(self.vm.newClass.students)
}
}
}
Output:
Test[4298:344875] [Agent] Received display message [Test.Student(id:
D1410829-F039-4D15-8440-69DEF0D55A26, name: ""), Test.Student(id:
50D45CC7-8144-49CC-88BE-598C890F2D4D, name: "")]

Related

Thread 1: Fatal error: Index out of range In SwiftUI

i am trying to make a small Social Media app. the friends and friendrequest gets stored as User in different arrays. But when i want to loop the array it an shows which user send a request it first works but when i accept the user and he is remove from the Array i am getting this error "Thread 1: Fatal error: Index out of range" i know its because the loop wants to loop to a index which doesn't exist anymore but how do i fix it ?
struct FriendsView: View {
#EnvironmentObject var appUser: User
var body: some View {
List {
ForEach(0..<appUser.friendAnfrage.count) {
durchlauf in
SingleFriendView(user: appUser.friendAnfrage[durchlauf])
}
}
}
}
class User: ObservableObject{
#Published var username: String = ""
#Published var name: String = ""
var password: String = ""
#Published var email: String = ""
#Published var beschreibung: String = ""
#Published var profilBild: UIImage?
#Published var friends = [User]()
#Published var friendAnfrage = [User]()
#Published var anfrageGesendet = [User]()
#Published var feed = [SinglePostView]()
func addFriend(friend: User,appUser: User) {
friend.friendAnfrage.append(appUser)
appUser.anfrageGesendet.append(friend)
}
func newFriend(newFriend: User) {
friends.append(newFriend)
for i in 0..<friendAnfrage.count {
if friendAnfrage[i].username == newFriend.username {
friendAnfrage.remove(at: i)
}
}
}
func friendAnfrage(friend: User,appUser: User) {
appUser.friendAnfrage.append(friend)
}
func makePost(image: UIImage,appUser: User) {
feed.append(SinglePostView(bild: image, ersteller: appUser))
for i in 0..<friends.count {
friends[i].feed.append(SinglePostView(bild: image, ersteller: appUser))
}
}
}
ForEach with an index-based approach is dangerous in SwiftUI. Instead, make your model identifiable.
class User: ObservableObject, Identifiable {
var id = UUID()
//...
Then, change your loop:
ForEach(appUser.friendAnfrage) { item in
SingleFriendView(user: item)
}
Unrelated to this exact issue, but generally SwiftUI does better with using a struct for a model instead of a class. If a User in friends is updated with your current code, because it's a nested ObservableObject, your View will not get automatically updated.
User should be a struct and ForEach isn't a traditional loop, it's a View that must be supplied identifiable data, e.g.
struct FriendsView: View {
#EnvironmentObject var model: Model
var body: some View {
List {
ForEach($model.users) { $user in
SingleFriendView(user: $user)
}
}
}
}
struct User: Identifiable{
let id = UUID()
var username: String = ""
var friends: [UUID] = []
}
class Model: ObservableObject {
#Published var users: [User] = []
}

SwiftUI - Should you use `#State var` or `let` in child view when using ForEach

I think I've a gap in understanding what exactly #State means, especially when it comes to displaying contents from a ForEach loop.
My scenario: I've created minimum reproducible example. Below is a parent view with a ForEach loop. Each child view has aNavigationLink.
// Parent code which passes a Course instance down to the child view - i.e. CourseView
struct ContentView: View {
#StateObject private var viewModel: ViewModel = .init()
var body: some View {
NavigationView {
VStack {
ForEach(viewModel.courses) { course in
NavigationLink(course.name + " by " + course.instructor) {
CourseView(course: course, viewModel: viewModel)
}
}
}
}
}
}
class ViewModel: ObservableObject {
#Published var courses: [Course] = [
Course(name: "CS101", instructor: "John"),
Course(name: "NS404", instructor: "Daisy")
]
}
struct Course: Identifiable {
var id: String = UUID().uuidString
var name: String
var instructor: String
}
Actual Dilemma: I've tried two variations for the CourseView, one with let constant and another with a #State var for the course field. Additional comments in the code below.
The one with the let constant successfully updates the child view when the navigation link is open. However, the one with #State var doesn't update the view.
struct CourseView: View {
// Case 1: Using let constant (works as expected)
let course: Course
// Case 2: Using #State var (doesn't update the UI)
// #State var course: Course
#ObservedObject var viewModel: ViewModel
var body: some View {
VStack {
Text("\(course.name) by \(course.instructor)")
Button("Edit Instructor", action: editInstructor)
}
}
// Case 1: It works and UI gets updated
// Case 2: Doesn't work as is.
// I've to directly update the #State var instead of updating the clone -
// which sometimes doesn't update the var in my actual project
// (that I'm trying to reproduce). It definitely works here though.
private func editInstructor() {
let instructor = course.instructor == "Bob" ? "John" : "Bob"
var course = course
course.instructor = instructor
save(course)
}
// Simulating a database save, akin to something like GRDB
// Here, I'm just updating the array to see if ForEach picks up the changes
private func save(_ courseToSave: Course) {
guard let index = viewModel.courses.firstIndex(where: { $0.id == course.id }) else {
return
}
viewModel.courses[index] = courseToSave
}
}
What I'm looking for is the best practice for a scenario where looping through an array of models is required and the model is updated in DB from within the child view.
Here is a right way for you, do not forget that we do not need put logic in View! the view should be dummy as possible!
struct ContentView: View {
#StateObject private var viewModel: ViewModel = ViewModel.shared
var body: some View {
NavigationView {
VStack {
ForEach(viewModel.courses) { course in
NavigationLink(course.name + " by " + course.instructor, destination: CourseView(course: course, viewModel: viewModel))
}
}
}
}
}
struct CourseView: View {
let course: Course
#ObservedObject var viewModel: ViewModel
var body: some View {
VStack {
Text("\(course.name) by \(course.instructor)")
Button("Update Instructor", action: { viewModel.update(course) })
}
}
}
class ViewModel: ObservableObject {
static let shared: ViewModel = ViewModel()
#Published var courses: [Course] = [
Course(name: "CS101", instructor: "John"),
Course(name: "NS404", instructor: "Daisy")
]
func update(_ course: Course) {
guard let index = courses.firstIndex(where: { $0.id == course.id }) else {
return
}
courses[index] = Course(name: course.name, instructor: (course.instructor == "Bob") ? "John" : "Bob")
}
}
struct Course: Identifiable {
let id: String = UUID().uuidString
var name: String
var instructor: String
}

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)
}
}
}
}

Why published Array does not change after I change him in SwiftUI?

So, this is just a sample code from my project. After I call SecondView, I want to change names in the array to "LoL", and display them. Why init() does not change my array? Since it does not display new names
struct Person: Identifiable {
let id = UUID()
var name: String
var index: Int
}
class User: ObservableObject {
#Published var array = [Person(name: "Nick", index: 0),
Person(name: "John", index: 1)
]
}
struct ContentView: View {
#ObservedObject var user = User()
var body: some View {
VStack {
ForEach (user.array) { row in
SecondView(name: row.name, index: row.index)
}
}
}
}
struct SecondView: View {
#ObservedObject var user = User()
var name = ""
var index = 0
init() {
user.array[index].name = "LoL"
}
init(name: String, index: Int) {
self.name = name
self.index = index
}
var body: some View {
VStack {
Text(name)
}
}
}
It is because you are not calling init() method in SecondView but you are calling init(name:, index:). Notice how you use SecondView initializer inside your FirstView's iteration (ie. ForEach) loop.
Also your second view displays the name that is passed along the initializer init(name:index:), not the one from your user array. So, if you want to change name to something, do that before this init(name:index:) is called, and pass the name from user array.
You can do it inside your first view.
struct ContentView: View {
#ObservedObject var user = User()
init() {
user.array[0].name = "LoL"
}
var body: some View {
VStack {
ForEach (user.array) { row in
SecondView(name: row.name, index: row.index)
}
}
}
}
Notice that it will now change the first name to Lol because we change it inside ContentView's initializer which then uses the same modified name.

Textfield in Foreach with EnvironmentObject

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 ?

Resources