SwiftUI - Refresh view on model updated in view model array - ios

Simple question to ask but I probably forgot something in the code.
Let's explain better using an image:
I need basically to update "COUNT" and "PRICE" while selecting/deselecting items.
I have a code structure like this:
Model:
class ServiceSelectorModel: Identifiable, ObservableObject {
var id = UUID()
var serviceName: String
var price: Double
init(serviceName: String, price: Double) {
self.serviceName = serviceName
self.price = price
}
#Published var selected: Bool = false
}
ViewModel:
class ServiceSelectorViewModel: ObservableObject {
#Published var services = [ServiceSelectorModel]()
init() {
self.services = [
ServiceSelectorModel(serviceName: "SERVICE 1", price: 1.80),
ServiceSelectorModel(serviceName: "SERVICE 2", price: 10.22),
ServiceSelectorModel(serviceName: "SERVICE 3", price: 2.55)
]
}
}
ToggleView
struct ServiceToggleView: View {
#ObservedObject var model: ServiceSelectorModel
var body: some View {
VStack(alignment: .center) {
HStack {
Text(model.serviceName)
Toggle(isOn: $model.selected) {
Text(String(format: "€ +%.2f", model.price))
.frame(maxWidth: .infinity, alignment: .trailing)
}
}
.background(model.selected ? Color.yellow : Color.clear)
}
}
}
ServiceSelectorView
struct ServiceSelectorView: View {
#ObservedObject var serviceSelectorVM = ServiceSelectorViewModel()
var body: some View {
VStack {
VStack(alignment: .leading) {
ForEach (serviceSelectorVM.services, id: \.id) { service in
ServiceToggleView(model: service)
}
}
let price = serviceSelectorVM.services.filter{ $0.selected }.map{ $0.price }.reduce(0, +)
Text("SELECTED: \(serviceSelectorVM.services.filter{ $0.selected }.count)")
Text(String(format: "TOTAL PRICE: €%.2f", price))
}
}
}
In this code I'm able to update the selected status of the model but the view model that contains all the models and should refresh the PRICE not updates.
Seems that the model in the array doesn't change.
what have i forgotten?

Probably most simple is to make your model as value type, then changing its properties the view model, holding it, will be updated. And to update view to use binding to those values
struct ServiceSelectorModel: Identifiable {
var id = UUID()
var serviceName: String
var price: Double
init(serviceName: String, price: Double) {
self.serviceName = serviceName
self.price = price
}
var selected: Bool = false
}
struct ServiceToggleView: View {
#Binding var model: ServiceSelectorModel
...
}
...
ForEach (serviceSelectorVM.services.indices, id: \.self) { i in
ServiceToggleView(model: $serviceSelectorVM.services[i])
}
Note: written inline, not tested, some typo might needed to be fixed

You have created two different Single source of truth if you notice carefully. That’s the reason parent is not updating, as it’s not linked to child.
One way is to create ServiceSelectorModel as a struct, and pass services array as #Binding in child view. Below is the working example.
ViewModel-:
struct ServiceSelectorModel {
var id = UUID().uuidString
var serviceName: String
var price: Double
var isSelected:Bool = false
init(serviceName: String, price: Double) {
self.serviceName = serviceName
self.price = price
}
}
class ServiceSelectorViewModel: ObservableObject,Identifiable {
#Published var services = [ServiceSelectorModel]()
var id = UUID().uuidString
init() {
self.services = [
ServiceSelectorModel(serviceName: "SERVICE 1", price: 1.80),
ServiceSelectorModel(serviceName: "SERVICE 2", price: 10.22),
ServiceSelectorModel(serviceName: "SERVICE 3", price: 2.55)
]
}
}
Views-:
struct ServiceToggleView: View {
#Binding var model: [ServiceSelectorModel]
var body: some View {
VStack(alignment: .center) {
ForEach(0..<model.count) { index in
HStack{
Text(model[index].serviceName)
Toggle(isOn: $model[index].isSelected) {
Text(String(format: "€ +%.2f", model[index].price))
.frame(maxWidth: .infinity, alignment: .trailing)
}
}.background(model[index].isSelected ? Color.yellow : Color.clear)
}
}
}
}
struct ServiceSelectorView: View {
#ObservedObject var serviceSelectorVM = ServiceSelectorViewModel()
var body: some View {
VStack {
VStack(alignment: .leading) {
ServiceToggleView(model: $serviceSelectorVM.services)
}
let price = serviceSelectorVM.services.filter{ $0.isSelected }.map{ $0.price }.reduce(0, +)
Text("SELECTED: \(serviceSelectorVM.services.filter{ $0.isSelected }.count)")
Text(String(format: "TOTAL PRICE: €%.2f", price))
}
}
}

Related

Changing a boolean in a variable from Identifiable struct

import Foundation
import SwiftUI
struct Item: Identifiable, Codable{
var id = UUID()
var image: String
var name: String
var price: Int
var isFavorite: Bool
}
class Model: ObservableObject{
#Published var group = [Item]() {
didSet {
if let encoded = try? JSONEncoder().encode(group){
UserDefaults.standard.set(encoded, forKey: "peopleKey")
}
}
}
init(){
if let savedItems = UserDefaults.standard.data(forKey: "peopleKey"),
let decodedItems = try? JSONDecoder().decode([Item].self, from: savedItems) {
group = decodedItems
} else {
group = itemData
}
}
var itemData: [Item] = [
Item(image: "imageGIFT", name: "Flower",price: 5 , isFavorite: false),
Item(image: "imageGIFT", name: "Coffe Cup",price: 9 , isFavorite: false),
Item(image: "imageGIFT", name: "Teddy Bear",price: 2 , isFavorite: false),
Item(image: "imageGIFT", name: "Parfume",price: 8 , isFavorite: false)
]
}
I am trying to change variables on this struct and I define as var but after encode and decode they has been let. I changed let part to var then Xcode gived an error.
here is my test code that shows doing model.people[0].myPerson.toggle() in the Button
does work. I have made some minor mods and added some comments to the code.
I suggest again, read the very basics of Swift,
in particular the array section at: https://docs.swift.org/swift-book/LanguageGuide/CollectionTypes.html.
Without understanding these very basic concepts you will keep struggling to code your App.
Note, there is probably no need for the myPeople array in your Model, but if that's what you want to have.
struct Person: Identifiable{
let id = UUID()
var name: String
var age: Int
var job: String
var myPerson: Bool
}
class Model: ObservableObject {
#Published var people: [Person] = []
#Published var myPeople: [Person] = []
init(){
addPeople()
}
// completely useless
func addPeople(){
people = peopleData
myPeople = peopleData.filter { $0.myPerson }
}
// here inside the class and using `Person` not `person`
var peopleData = [
Person(name: "Bob", age: 22, job: "Student", myPerson: false),
Person(name: "John", age: 26, job: "Chef", myPerson: false)
]
}
struct ContentView: View {
#StateObject var model = Model()
var body: some View {
VStack {
VStack {
// this `myPeople` array is empty at first, nothing is displayed
ForEach(model.myPeople) { person in
VStack(alignment: .leading){
Text("Name: \(person.name)").foregroundColor(.blue)
Text("Age: \(person.age)").foregroundColor(.blue)
Text("Job: \(person.job)").foregroundColor(.blue)
Text("myPerson: " + String(person.myPerson)).foregroundColor(.blue)
}.padding()
}
}
VStack {
// this `people` array has two items in it
ForEach(model.people) { person in
VStack(alignment: .leading){
Text("Name: \(person.name)").foregroundColor(.red)
Text("Age: \(person.age)").foregroundColor(.red)
Text("Job: \(person.job)").foregroundColor(.red)
Text("myPerson: " + String(person.myPerson)).foregroundColor(.red)
}.padding()
}
}
Button("Click") {
print("\n--> before name: \(model.people[0].name) ")
print("--> before myPerson: \(model.people[0].myPerson) ")
model.people[0].name = "Franz"
model.people[0].myPerson.toggle()
print("\n--> after name: \(model.people[0].name) ")
print("--> after myPerson: \(model.people[0].myPerson) ")
// update the myPeople array (the blue items)
model.myPeople = model.people.filter { $0.myPerson }
}
}
}
}
Alternatively, you could use this code using only one array of people: [Person]:
class Model: ObservableObject {
#Published var people: [Person] = []
init(){
addPeople()
}
func addPeople() {
people = peopleData
}
// here inside the class and using `Person` not `person`
var peopleData = [
Person(name: "Bob", age: 22, job: "Student", myPerson: false),
Person(name: "John", age: 26, job: "Chef", myPerson: false)
]
}
struct ContentView: View {
#StateObject var model = Model()
var body: some View {
VStack {
VStack {
// here filter on myPerson=true
ForEach(model.people.filter { $0.myPerson }) { person in
VStack(alignment: .leading){
Text("Name: \(person.name)").foregroundColor(.blue)
Text("Age: \(person.age)").foregroundColor(.blue)
Text("Job: \(person.job)").foregroundColor(.blue)
Text("myPerson: " + String(person.myPerson)).foregroundColor(.blue)
}.padding()
}
}
VStack {
// here filter on myPerson=false
ForEach(model.people.filter { !$0.myPerson }) { person in
VStack(alignment: .leading){
Text("Name: \(person.name)").foregroundColor(.red)
Text("Age: \(person.age)").foregroundColor(.red)
Text("Job: \(person.job)").foregroundColor(.red)
Text("myPerson: " + String(person.myPerson)).foregroundColor(.red)
}.padding()
}
}
Button("Click") {
model.people[0].name = "Franz"
model.people[0].myPerson.toggle()
}
}
}
}

SwiftUI: Checkmarks disappear when changing from one view to another using NavigationLink

I'm trying to make an app that is displaying lists with selections/checkmarks based on clicked NavigationLink. The problem I encountered is that my selections disappear when I go back to main view and then I go again inside the NavigationLink. I'm trying to save toggles value in UserDefaults but it's not working as expected. Below I'm pasting detailed and main content view.
Second view:
struct CheckView: View {
#State var isChecked:Bool = false
#EnvironmentObject var numofitems: NumOfItems
var title:String
var count: Int=0
var body: some View {
HStack{
ScrollView {
Toggle("\(title)", isOn: $isChecked)
.toggleStyle(CheckToggleStyle())
.tint(.mint)
.onChange(of: isChecked) { value in
if isChecked {
numofitems.num += 1
print(value)
} else{
numofitems.num -= 1
}
UserDefaults.standard.set(self.isChecked, forKey: "locationToggle")
}.onTapGesture {
}
.onAppear {
self.isChecked = UserDefaults.standard.bool(forKey: "locationToggle")
}
Spacer()
}.frame(maxWidth: .infinity,alignment: .topLeading)
}
}
}
Main view:
struct CheckListView: View {
#State private var menu = Bundle.main.decode([ItemsSection].self, from: "items.json")
var body: some View {
NavigationView{
List{
ForEach(menu){
section in
NavigationLink(section.name) {
VStack{
ScrollView{
ForEach(section.items) { item in
CheckView( title: item.name)
}
}
}
}
}
}
}.navigationBarHidden(true)
.navigationViewStyle(StackNavigationViewStyle())
.listStyle(GroupedListStyle())
.navigationViewStyle(StackNavigationViewStyle())
}
}
ItemsSection:
[
{
"id": "9DC6D7CB-B8E6-4654-BAFE-E89ED7B0AF94",
"name": "Africa",
"items": [
{
"id": "59B88932-EBDD-4CFE-AE8B-D47358856B93",
"name": "Algeria"
},
{
"id": "E124AA01-B66F-42D0-B09C-B248624AD228",
"name": "Angola"
}
Model:
struct ItemsSection: Codable, Identifiable, Hashable {
var id: UUID = UUID()
var name: String
var items: [CountriesItem]
}
struct CountriesItem: Codable, Equatable, Identifiable,Hashable {
var id: UUID = UUID()
var name: String
}
As allready stated in the comment you have to relate the isChecked property to the CountryItem itself. To get this to work i have changed the model and added an isChecked property. You would need to add this to the JSON by hand if the JSON allread exists.
struct CheckView: View {
#EnvironmentObject var numofitems: NumOfItems
//use a binding here as we are going to manipulate the data coming from the parent
//and pass the complete item not only the name
#Binding var item: CountriesItem
var body: some View {
HStack{
ScrollView {
//use the name and the binding to the item itself
Toggle("\(item.name)", isOn: $item.isChecked)
.toggleStyle(.button)
.tint(.mint)
// you now need the observe the isChecked inside of the item
.onChange(of: item.isChecked) { value in
if value {
numofitems.num += 1
print(value)
} else{
numofitems.num -= 1
}
}.onTapGesture {
}
Spacer()
}.frame(maxWidth: .infinity,alignment: .topLeading)
}
}
}
struct CheckListView: View {
#State private var menu = Bundle.main.decode([ItemsSection].self, from: "items.json")
var body: some View {
NavigationView{
List{
ForEach($menu){ // from here on you have to pass a binding on to the decendent views
// mark the $ sign in front of the property name
$section in
NavigationLink(section.name) {
VStack{
ScrollView{
ForEach($section.items) { $item in
//Pass the complete item to the CheckView not only the name
CheckView(item: $item)
}
}
}
}
}
}
}.navigationBarHidden(true)
.navigationViewStyle(StackNavigationViewStyle())
.listStyle(GroupedListStyle())
.navigationViewStyle(StackNavigationViewStyle())
}
}
Example JSON:
[
{
"id": "9DC6D7CB-B8E6-4654-BAFE-E89ED7B0AF94",
"name": "Africa",
"items": [
{
"id": "59B88932-EBDD-4CFE-AE8B-D47358856B93",
"name": "Algeria",
"isChecked": false
},
{
"id": "E124AA01-B66F-42D0-B09C-B248624AD228",
"name": "Angola",
"isChecked": false
}
]
}
]
Remarks:
The aproach with JSON and storing this in the bundle will prevent you from persisting the isChecked property between App launches. Because you cannot write to the Bundle from within your App. The choice will persist as long as the App is active but will be back to default as soon as you either reinstall or force quit it.
As already mentioned in the comment, I don'r see where you read back from UserDefaults, so whatever gets stored there, you don't read it. But even if so, each Toggle is using the same key, so you are overwriting the value.
Instead of using the #State var isChecked, which is used just locally, I'd create another struct item which gets the title from the json and which contains a boolean that gets initialized with false.
From what I understood, I assume a solution could look like the following code. Just a few things:
I am not sure how your json looks like, so I am not loading from a json, I add ItemSections Objects with a title and a random number of items (actually just titles again) with a function.
Instead of a print with the number of checked toggles, I added a text output on the UI. It shows you on first page the number of all checked toggles.
Instead of using UserDefaults I used #AppStorage.
To make that work you have to make Array conform to RawRepresentable you achieve that with the following code/extension (just add it once somewhere in your project)
Maybe you should thing about a ViewModel (e.g. ItemSectionViewModel), to load the data from the json and provide it to the views as an #ObservableObject.
The code for the views:
//
// CheckItem.swift
// CheckItem
//
// Created by Sebastian on 24.08.22.
//
import SwiftUI
struct ContentView: View {
var body: some View {
VStack() {
CheckItemView()
}
}
}
struct CheckItemView: View {
let testStringForTestData: String = "Check Item Title"
#AppStorage("itemSections") var itemSections: [ItemSection] = []
func addCheckItem(title: String, numberOfItems: Int) {
var itemArray: [Item] = []
for i in 0...numberOfItems {
itemArray.append(Item(title: "item \(i)"))
}
itemSections.append(ItemSection(title: title, items: itemArray))
}
func getSelectedItemsCount() -> Int{
var i: Int = 0
for itemSection in itemSections {
let filteredItems = itemSection.items.filter { item in
return item.isOn
}
i = i + filteredItems.count
}
return i
}
var body: some View {
NavigationView{
VStack() {
List(){
ForEach(itemSections.indices, id: \.self){ id in
NavigationLink(destination: ItemSectionDetailedView(items: $itemSections[id].items)) {
Text(itemSections[id].title)
}
.padding()
}
}
Text("Number of checked items: \(self.getSelectedItemsCount())")
.padding()
Button(action: {
self.addCheckItem(title: testStringForTestData, numberOfItems: Int.random(in: 0..<4))
}) {
Text("Add Item")
}
.padding()
}
}
}
}
struct ItemSectionDetailedView: View {
#Binding var items: [Item]
var body: some View {
ScrollView() {
ForEach(items.indices, id: \.self){ id in
Toggle(items[id].title, isOn: $items[id].isOn)
.padding()
}
}
}
}
struct ItemSection: Identifiable, Hashable, Codable {
var id: String = UUID().uuidString
var title: String
var items: [Item]
}
struct Item: Identifiable, Hashable, Codable {
var id: String = UUID().uuidString
var title: String
var isOn: Bool = false
}
Here the adjustment to work with #AppStorage:
extension Array: RawRepresentable where Element: Codable {
public init?(rawValue: String) {
guard let data = rawValue.data(using: .utf8),
let result = try? JSONDecoder().decode([Element].self, from: data)
else {
return nil
}
self = result
}
public var rawValue: String {
guard let data = try? JSONEncoder().encode(self),
let result = String(data: data, encoding: .utf8)
else {
return "[]"
}
return 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

Updating Member of an Array

Newbie here.
My problem simplified:
I have a Person struct consisting of 2 strings - first and last name.
An initial array with a few persons (ex. "Bob" "Smith", "Joe" "Johnson", etc.)
A list view showing each member.
Clicking on a row in the list shows a detail view - call it "person card" view - which shows the first name and last name.
I then have a modal view to edit these variables.
Currently the Save button on the modal only closes the modal. However, because I am using bindings on the modal view to the values on the "person card" view, the "person card" view is updated with the changed data when the modal closes.
The list view though still shows the original value(s) and not the updated data (as I expect). I know that I have to add as method to the save function but I'm not sure what. I know how to insert and append to an array but I can't find an update array method.
FYI - The data model I am using is a "store" instance of a class that is an ObservableObject. I have that variable declared as an EnvironmentObject on each view.
Here is the code as requested:
struct PatientData: Identifiable
{
let id = UUID()
var patientName: String
var age: String
}
let patientDataArray: [PatientData] =
[
PatientData(patientName: "Charles Brown", age: "68"),
PatientData(patientName: "Jim Morrison", age: "36"),
]
final class PatientDataController: ObservableObject
{
#Published var patients = patientDataArray
{
struct PatientList: View
{
#EnvironmentObject var patientDataController: PatientDataController
#State private var showModalSheet = false
var body: some View
{
NavigationView
{
List
{
ForEach(patientDataController.patients)
{ patientData in NavigationLink(destination: PatientInfoCard(patientData: patientData))
{ PatientListCell(patientData: patientData) }
}
.onMove(perform: move)
.onDelete(perform: delete)
.navigationBarTitle(Text("Patient List"))
}
struct PatientInfoCard: View
{
#EnvironmentObject var patientDataController: PatientDataController
#State var patientData: PatientData
#State private var showModalSheet = false
var body: some View
{
VStack(alignment: .leading, spacing: 8)
{ // Change to patientDataArray???
Text(patientData.patientName)
.font(.largeTitle)
BasicInfo(patientData: patientData)
Spacer()
.frame(minWidth: 0, maxWidth: .infinity)
}
.padding()
// Can't push Edit button more than once
.navigationBarItems(trailing: Button(action:
{self.showModalSheet = true})
{Text("Edit")})
.sheet(isPresented: $showModalSheet)
{
EditPatientModal(patientData: self.$patientData, showModalSheet: self.$showModalSheet)
.environmentObject(self.patientDataController)
}
}
}
struct EditPatientModal: View
{
#Environment(\.presentationMode) var presentationMode
#EnvironmentObject var patientDataController: PatientDataController
#Binding var patientData: PatientData
#Binding var showModalSheet: Bool
var body: some View
{
NavigationView
{
VStack(alignment: .leading)
{
Text("Name")
.font(.headline)
TextField("enter name", text: $patientData.patientName)
Text("Age")
.font(.headline)
TextField("enter age", text: $patientData.age)
}
.navigationBarTitle(Text("Edit Patient"), displayMode: .inline)
.navigationBarItems(
leading: Button("Cancel")
{ self.cancel() },
trailing: Button("Save")
{ self.save() } )
}
}
private func save()
{
self.presentationMode.wrappedValue.dismiss()
}
Here is my updated code:
class PatientData: ObservableObject, Identifiable
{
let id = UUID()
#Published var patientName = ""
#Published var age = ""
init(patientName: String, age: String)
{
self.patientName = patientName
self.age = age
}
}
let patientDataArray: [PatientData] =
[
PatientData(patientName: "Charles Brown", age: "68"),
PatientData(patientName: "Jim Morrison", age: "36")
]
final class PatientDataController: ObservableObject
{
#Published var patients = patientDataArray
}
struct PatientList: View
{
#EnvironmentObject var patientDataController: PatientDataController
#EnvironmentObject var patientData: PatientData
#State private var showModalSheet = false
var body: some View
{
NavigationView
{
List
{
ForEach(self.patientDataController.patients.indices)
{ idx in
NavigationLink(destination: PatientInfoCard(patientData: self.$patientDataController.patients[idx]))
/*Cannot convert value of type 'Binding<PatientData>' to expected argument type 'PatientData'*/ <-- My one error message; in NavigationLink
{ PatientListCell(patientData: self.$patientDataController.patients[idx]) }
}
.onMove(perform: move)
.onDelete(perform: delete)
.navigationBarTitle(Text("Patient List"))
}
.navigationBarItems(leading: EditButton())
struct PatientInfoCard: View
{
#EnvironmentObject var patientDataController: PatientDataController
#Binding var patientData: PatientData
#State private var showModalSheet = false
var body: some View
{
VStack(alignment: .leading, spacing: 8)
{
Text(patientData.patientName)
.font(.largeTitle)
BasicInfo(patientData: patientData)
Spacer()
.frame(minWidth: 0, maxWidth: .infinity)
}
.padding()
.navigationBarItems(trailing: Button(action:
{self.showModalSheet = true})
{Text("Edit")})
.sheet(isPresented: $showModalSheet)
{
EditPatientModal(patientData: self.$patientData, showModalSheet: self.$showModalSheet)
.environmentObject(self.patientDataController)
}
}
}
struct BasicInfo: View
{
#EnvironmentObject var patientDataController: PatientDataController
#State var patientData: PatientData
var patientDataIndex: Int
{
patientDataController.patients.firstIndex(where: { $0.id == patientData.id })!
}
var body: some View
{
VStack(alignment: .leading, spacing: 8)
{
Text("Age:")
.font(.headline)
Text(patientData.age)
.font(.subheadline)
.foregroundColor(.secondary)
}
}
}
struct EditPatientModal: View
{
#Environment(\.presentationMode) var presentationMode
#EnvironmentObject var patientDataController: PatientDataController
#Binding var patientData: PatientData
#Binding var showModalSheet: Bool
var body: some View
{
NavigationView
{
VStack(alignment: .leading)
{
Text("Name")
.font(.headline)
TextField("enter name", text: $patientData.patientName)
Text("Age")
.font(.headline)
TextField("enter age", text: $patientData.age)
}
.navigationBarTitle(Text("Edit Patient"), displayMode: .inline)
.navigationBarItems(
leading: Button("Cancel")
{ self.cancel() },
trailing: Button("Save")
{ self.save() } )
}
}
private func save()
{
self.presentationMode.wrappedValue.dismiss()
}
You are missing 2 things in your code.
Your struct needs to be ObservableObject otherwise any changes happen to it will not get effected and in order for it to be ObservableObject it has to be a class so first change:
class PatientData: ObservableObject, Identifiable
{
let id = UUID()
#Published var patientName: String
#Published var age: String
init(patientName: String, age: String) {
self.patientName = patientName
self.age = age
}
}
I understand you have an environmentObject which is publishing, but it's only publishing changes to the array, meaning adding or removing items but not to individual patientData objects.
2nd thing to change is in your forEach loop you need pass Patient as a Bind and in order to do that you have to loop through indices and then access the data through Bind
NavigationView
{
if(self.patientDataController.patients.count > 0) {
List {
ForEach(self.patientDataController.patients.enumerated().map({$0}), id:\.element.id) { idx, patient in
NavigationLink(destination: PatientInfoCard(patientData: self.$patientDataController.patients[idx])) {
Text(patient.patientName)
}
}
}
.navigationBarItems(leading: EditButton())
} else {
Text("List is empty")
}
}
Let us know if this doesn't work

List reload animation glitches

So I have a list that changes when user fill in search keyword, and when there is no result, all the cells collapse and somehow they would fly over to the first section which looks ugly. Is there an error in my code or is this an expected SwiftUI behavior? Thanks.
import SwiftUI
struct ContentView: View {
#ObservedObject var viewModel = ViewModel(photoLibraryService: PhotoLibraryService.shared)
var body: some View {
NavigationView {
List {
Section {
TextField("Enter Album Name", text: $viewModel.searchText)
}
Section {
if viewModel.libraryAlbums.count > 0 {
ForEach(viewModel.libraryAlbums) { libraryAlbum -> Text in
let title = libraryAlbum.assetCollection.localizedTitle ?? "Album"
return Text(title)
}
}
}
}.listStyle(GroupedListStyle())
.navigationBarTitle(
Text("Albums")
).navigationBarItems(trailing: Button("Add Album", action: {
PhotoLibraryService.shared.createAlbum(withTitle: "New Album \(Int.random(in: 1...100))")
}))
}.animation(.default)
}
}
1) you have to use some debouncing to reduce the needs to refresh the list, while typing in the search field
2) disable animation of rows
The second is the hardest part. the trick is to force recreate some View by setting its id.
Here is code of simple app (to be able to test this ideas)
import SwiftUI
import Combine
class Model: ObservableObject {
#Published var text: String = ""
#Published var debouncedText: String = ""
#Published var data = ["art", "audience", "association", "attitude", "ambition", "assistance", "awareness", "apartment", "artisan", "airport", "atmosphere", "actor", "army", "attention", "agreement", "application", "agency", "article", "affair", "apple", "argument", "analysis", "appearance", "assumption", "arrival", "assistant", "addition", "accident", "appointment", "advice", "ability", "alcohol", "anxiety", "ad", "activity"].map(DataRow.init)
var filtered: [DataRow] {
data.filter { (row) -> Bool in
row.txt.lowercased().hasPrefix(debouncedText.lowercased())
}
}
var id: UUID {
UUID()
}
private var store = Set<AnyCancellable>()
init(delay: Double) {
$text
.debounce(for: .seconds(delay), scheduler: RunLoop.main)
.sink { [weak self] (s) in
self?.debouncedText = s
}.store(in: &store)
}
}
struct DataRow: Identifiable {
let id = UUID()
let txt: String
init(_ txt: String) {
self.txt = txt
}
}
struct ContentView: View {
#ObservedObject var search = Model(delay: 0.5)
var body: some View {
NavigationView {
VStack(alignment: .leading) {
TextField("filter", text: $search.text)
.padding(.vertical)
.padding(.horizontal)
List(search.filtered) { (e) in
Text(e.txt)
}.id(search.id)
}.navigationBarTitle("Navigation")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
and i am happy with the result

Resources