ForEach TextField in SwiftUI - ios

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

Related

SwiftUI #Binding property not updating nested views - Xcode 12

I have problems updating my nested SwiftUI views with #Binding property.
I declared a DataModel with ObservableObject protocol:
class DataModel: ObservableObject {
#Published var subjects: [Subject] = []
...
}
I added it to my main app:
#main
struct LessonToTextApp: App {
#ObservedObject private var data = DataModel()
var body: some Scene {
WindowGroup {
NavigationView {
SubjectsView(subjects: $data.subjects) {
data.save()
}
}
.onAppear {
data.load()
}
}
}
}
I passed the Subjects array to the first view
struct SubjectsView: View {
#Binding var subjects: [Subject]
var body: some View {
List {
if subjects.isEmpty {
Text("subjects.empty")
} else {
ForEach(subjects) { subject in
NavigationLink(destination: DetailView(subject: binding(for: subject), saveAction: saveAction)) {
CardView(subject: subject)
}
.listRowBackground(subject.color)
.cornerRadius(10)
}
}
}
private func binding(for subject: Subject) -> Binding<Subject> {
guard let subIndex = subjects.firstIndex(where: { $0.id == subject.id }) else {
fatalError("Can't find subject in array")
}
return $subjects[subIndex]
}
And then i passed the single subject to the Second view using the function binding declared above:
struct DetailView: View {
#Binding var subject: Subject
var body: some View {
ForEach(subject.lessons) { lesson in
NavigationLink(destination: LessonView(lesson: lesson)) {
Text(lesson.date, style: .date)
}
}
.onDelete { indexSet in
self.subject.lessons.remove(atOffsets: indexSet)
}
}
In the DetailView, when i delete an item in ForEach the item still appear, the view doesn't update.
I'm using SwiftUI 2.0 on Xcode 12.3 (12C33)
EDIT
This is the Model:
struct Subject: Identifiable, Codable {
let id: UUID
var name: String
var teacher: String
var color: Color
var lessons: [Lesson]
}
struct Lesson: Identifiable, Codable {
let id: UUID
let date: Date
var lenghtInMinutes: Int
var transcript: String
}
Your SubjectsView should take in the entire DataModel as an #EnvironmentObject
struct LessonToTextApp: App {
#StateObject private var data = DataModel()
var body: some Scene {
WindowGroup {
NavigationView {
SubjectsView().environmentObject(data) {
data.save()
}
...
struct SubjectsView: View {
#EnvironmentObject var data = DataModel
var body: some View {
List {
if data.subjects.isEmpty {
Text("subjects.empty")
...
Also, struct are immutable
class Subject: Identifiable, Codable, ObservableObject {
let id: UUID
#Published var name: String
#Published var teacher: String
#Published var color: Color
...
}
struct DetailView: View {
#ObservedObject var subject: Subject
var body: some View {
...
That way you can get to the DetailView with
DetailView(subject: subject)

Swift Array .append() method not working in SwiftUI

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: "")]

For a List-Detail interface - Data is updated in the Detail View, and the Data is changed but not immediately reflected in the Detail view

I am using SwiftUI on the Apple Watch and trying to use #ObservableObject, #ObservedObject, and #Binding correctly. I'm updating a value in a DetailView, and I want to have it reflected locally, as well as have the data changed globally. The code below works, but I am using a kludge to force the DetailView to redraw itself:
Is there a better way?
-------------- ContentView.swift ---------------
import Combine
import SwiftUI
struct person: Identifiable {
var id:Int = 0
var name:String
init( id: Int, name:String) {
self.id = id
self.name = name
}
}
class AppData: ObservableObject {
#Published var people:[person] = [person(id:0, name:"John"),
person(id:1, name:"Bret"),
person(id:2,name:"Sue"),
person(id:3,name:"Amy")]
}
var gAppData = AppData()
struct ContentView: View {
#ObservedObject var model:AppData
var body: some View {
List( model.people.indices ){ index in
NavigationLink(destination: DetailView(person:self.$model.people[index])) { Text(self.model.people[index].name) }
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(model:gAppData)
}
}
-------------- DetailView.swift ---------------
import SwiftUI
struct DetailView: View {
#Binding var person: person
// Created an unnecessary var to force a redreaw of the view
#State var doRedraw:Bool = true
var body: some View {
VStack(){
Text(person.name)
Button(action:{ self.person.name = "Bob"; self.doRedraw = false }) {
Text("Set Name to Bob")
}
}
}
}
struct DestView_Previews: PreviewProvider {
static var previews: some View {
DetailView(person:.constant(person( id:0, name:"John"))) // what does ".constant" actually do?
}
}
The problem here is because your view redraws only when you changes the #State or #Binding variable. Here you do not change the Person variable, but its property, which should not affect the user interface (because you didn't say to do this). I changed your code for a little for showing how to achieve this effect, you can go ahead from this point. You need to remember, what exactly affect UI:
class Person: Identifiable, ObservableObject { // better to assign struct/class names using UpperCamelCase
#Published var name:String // now change of this variable will affect UI
var id:Int = 0
init( id: Int, name:String) {
self.id = id
self.name = name
}
}
// changes in DetailView
struct DetailView: View {
#EnvironmentObject var person: Person
var body: some View {
VStack(){
Text(person.name)
Button(action:{ self.person.name = "Bob" }) {
Text("Set Name to Bob")
}
}
}
}
// preview
struct DetailViewWithoutGlobalVar_Previews: PreviewProvider {
static var previews: some View {
DetailView()
.environmentObject(Person(id: 1, name: "John"))
}
}
update: full code for List and Detail
import SwiftUI
class Person: Identifiable, ObservableObject { // better to assign type names using UpperCamelCase
#Published var name: String //{
var id: Int = 0
init( id: Int, name:String) {
self.id = id
self.name = name
}
func changeName(_ newName: String) {
self.name = newName
}
}
class AppData: ObservableObject {
#Published var people: [Person] = [Person(id:0, name:"John"),
Person(id:1, name:"Bret"),
Person(id:2,name:"Sue"),
Person(id:3,name:"Amy")]
}
struct ContentViewWithoutGlobalVar: View {
#EnvironmentObject var model: AppData
var body: some View {
NavigationView { // you forget something to navigate between views
List(model.people.indices) { index in
NavigationLink(destination: DetailView()
.environmentObject(self.model.people[index])) {
PersonRow(person: self.$model.people[index])
}
}
}
}
}
struct PersonRow: View {
#Binding var person: Person // this struct will see changes in Person and show them
var body: some View {
Text(person.name)
}
}
struct DetailView: View {
#EnvironmentObject var person: Person
var body: some View {
VStack(){
Text(self.person.name)
Button(action:{ self.person.changeName("Bob") }) {
Text("Set Name to Bob")
}
}
}
}
struct ContentViewWithoutGlobalVar_Previews: PreviewProvider {
static var previews: some View {
Group {
ContentViewWithoutGlobalVar()
.environmentObject(AppData())
DetailView()
.environmentObject(Person(id: 0, name: "John"))
}
}
}

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 ?

How to observe a TextField value with SwiftUI and Combine?

I'm trying to execute an action every time a textField's value is changed.
#Published var value: String = ""
var body: some View {
$value.sink { (val) in
print(val)
}
return TextField($value)
}
But I get below error.
Cannot convert value of type 'Published' to expected argument type 'Binding'
This should be a non-fragile way of doing it:
class MyData: ObservableObject {
var value: String = "" {
willSet(newValue) {
print(newValue)
}
}
}
struct ContentView: View {
#ObservedObject var data = MyData()
var body: some View {
TextField("Input:", text: $data.value)
}
}
In your code, $value is a publisher, while TextField requires a binding. While you can change from #Published to #State or even #Binding, that can't observe the event when the value is changed.
It seems like there is no way to observe a binding.
An alternative is to use ObservableObject to wrap your value type, then observe the publisher ($value).
class MyValue: ObservableObject {
#Published var value: String = ""
init() {
$value.sink { ... }
}
}
Then in your view, you have have the binding $viewModel.value.
struct ContentView: View {
#ObservedObject var viewModel = MyValue()
var body: some View {
TextField($viewModel.value)
}
}
I don't use combine for this. This it's working for me:
TextField("write your answer here...",
text: Binding(
get: {
return self.query
},
set: { (newValue) in
self.fetch(query: newValue) // any action you need
return self.query = newValue
}
)
)
I have to say it's not my idea, I read it in this blog: SwiftUI binding: A very simple trick
If you want to observe value then it should be a State
#State var value: String = ""
You can observe TextField value by using ways,
import SwiftUI
import Combine
struct ContentView: View {
#State private var Text1 = ""
#State private var Text2 = ""
#ObservedObject var viewModel = ObserveTextFieldValue()
var body: some View {
//MARK: TextField with Closures
TextField("Enter text1", text: $Text1){
editing in
print(editing)
}onCommit: {
print("Committed")
}
//MARK: .onChange Modifier
TextField("Enter text2", text: $Text2).onChange(of: Text2){
text in
print(text)
}
//MARK: ViewModel & Publisher(Combine)
TextField("Enter text3", text: $viewModel.value)
}
}
class ObserveTextFieldValue: ObservableObject {
#Published var value: String = ""
private var cancellables = Set<AnyCancellable>()
init() {
$value.sink(receiveValue: {
val in
print(val)
}).store(in: &cancellables)
}
}
#Published is one of the most useful property wrappers in SwiftUI, allowing us to create observable objects that automatically announce when changes occur that means whenever an object with a property marked #Published is changed, all views using that object will be reloaded to reflect those changes.
import SwiftUI
struct ContentView: View {
#ObservedObject var textfieldData = TextfieldData()
var body: some View {
TextField("Input:", text: $textfieldData.data)
}
}
class TextfieldData: ObservableObject{
#Published var data: String = ""
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Resources