Cannot get swift view to update within multiple Lists and ForEach loop - foreach

I am trying to get a my contentview to update correctly when I change a property in a child view. The view displays all the data correctly, but does not update until you back up a level in the UI and go back to the child view. The child view that does not update is embedded in NavigationView -> List -> Section -> NavigationLink -> List -> ForEach -> NavigationLink.
The same child view updates content view correctly when embedded in NavigationView -> List -> Section -> ForEach -> NavigationLink.
userManager is an instance of a class containing my data, added to the environment
user is an instance of NSManagedObject stored in userManager as an array, and created by a FetchRequest from a Core Data store
UserDetail is a view displaying properties of user, accessing userManager through #EnvironmentObject.
I have tried adding the id:.id and id:.self to the ForEach loop and did not work, I started with passing the userManager in as an #ObservedObject but that did not work either.
I want to keep this view structure because it displays my data nicely the way I want it in the UI, just does not update when the user property is changed.
I am just learning SwiftUI, I am sure I am missing something obvious but I cannot figure it out after searching.
I am attaching code snippets below:
var body: some View {
NavigationView {
List {
Section("Active Users"){
ForEach(userManager.activeUsers) {user in
NavigationLink {
UserDetail( user: user) //*** this updates contentview correctly when user property is updated, this prints a confirmation to the console when user property is changed.
} label: {
Text(user.wrappedName)
}
}
}
Section("Active Users"){
NavigationLink {
List {
ForEach(userManager.activeUsers, id:\.id) { user in
NavigationLink {
UserDetail( user: user) //** this does not update content view when same property in user is updated. I prove this by a print statement which is not called.
} label: {
Text(user.wrappedName)
}
}
}
} label: {
Text("Last Names A through H")
}
}
Here is the beginning of my UserManager class, I am using a number of computed properties to return subsets of the users array.
class UserManager: ObservableObject{
#Published var users: [UserCore]
init() {
self.users = [UserCore]()
}
Here is the beginning of UserDetail View:
struct UserDetail: View {
#Environment(\.managedObjectContext) var moc
#Environment(\.presentationMode) var presentationMode
#EnvironmentObject var userManager: UserManager
var user: UserCore

I just figured it out!
I programmatically ask the Button that changes the user state to send an objectWillChange.send() and that forces the content view to update and gives me the functionality I want:
Button(user.isActive ? "Deactivate User" : "Activate User"){
user.isActive.toggle()
userManager.objectWillChange.send() // this fixed the problem!
try? moc.save()
presentationMode.wrappedValue.dismiss()

Related

Swift: (How) Can I use two different Data Model Structs in one View?

I have a UI with a list of items that the user can tap. This opens a detail view listing all details for one item.
However, I want to include 2 values in that detail view that are stored in a different collection in Firestore and that also have their own Data Model struct. The reason for this is that a different app works with that collection and I want to separate "shared" collections from the rest.
I got a function that is pulling these 2 values from Firestore done
This function is passing the values to a struct called CareData in my Data Model done
I think I set up everything correctly in the detail view, but the problem is passing that data from the tabable list to the detail view.
Let me try to explain what I did with my code:
Data Model
Just simple arrays, nothing complex.
struct Items: Decodable, Identifiable {
var id: String
var name: String
…
}
struct CareData {
var avHeight: Int
var avWater: Int
}
Detail View
struct Detail: View {
#EnvironmentObject var model: ViewModel
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
// Passing the model instances
let item: Items
let care: CareData
var body: some View {
// Some (working) code where data like item.name is shown.
...
// The 2 values using the CareData Data Model
Text("Height: \(care.avHeight)")
Text("Water: \(care.avWater)")
}
}
View Model
An important note here: The documents in my shared collection are named after the item name of the not shared collection. When I call the function, I use item.name to query to the correct document in the shared collection.
class ViewModel: ObservableObject {
#Published var itemList = [Items]()
#Published var careData = [CareData]()
...
func getCareData(item: String) {
// Some code that gets the data in Firestore from the shared collection and appends it to careData. It is working all well.
}
}
Problematic List View
Detail() in the NavigationLink is expecting me to pass 2 parameters because I am trying to pass both model instances in the Detail View. I understand to use item: item, as I am looping through all items and need to define what is needed for the Detail View. But what do I need to add for care:?
struct PlantList: View {
#EnvironmentObject var model: ViewModel
var body: some View {
ForEach(model.itemList) { item in
NavigationLink(destination: Detail(item: item, care: ?????? )) { // XCode proposing to go for "CareData", but then throws the error "Cannot convert value of type 'CareData.Type' to expected argument type 'CareData'"
Text(item.name)
}
.onAppear {
model.getCareData(item: item.name)
}
}
}
}
I tried weird things like CareDate.init(avHeight: , avWater:) and it worked when I wrote numbers straight into the code, but I need the variables to be there not some static numbers I came up with.
I hope someone can help. All I want is to show the 2 values in the detail view. This is probably a stupid issue, but I'm frustrated as I seem to not understand the very basics of Swift programming yet.
try something like this (untested of course, since I don't have your database), if you only ever want the first element:
EDIT-1: update
struct PlantList: View {
#EnvironmentObject var model: ViewModel
var body: some View {
ForEach(model.itemList) { item in
NavigationLink(destination: Detail(item: item, care: model.careData.first ?? CareData(avHeight: 0, avWater: 0))) {
Text(item.name)
}
.onAppear {
model.getCareData(item: item.name)
}
}
}
}

SwiftUI list item not updated if model is wrapped in #State

Given
a View with a simple List
an ItemView for each element of the list
a Model for the app
a model value (Deck)
Tapping on a button in the main view, is expected the model to change and propagate the changes to the ItemView.
The problem is that the changes only propagate if the model struct is stored in the ItemView as a normal variable; but if i add the #State property wrapper these do not happen. The view will update but not change (like if the data has been cached).
Question 1: is this an expected behaviour? If so, why? I was expecting to have the ItemView to only update when the model change by observing it throw #State, this way instead the view will always refresh whenever the list commands it, even if the data is not updated?
Question 2: Is it normal otherwise to have the items of a list using plain structs properties as models? Using observable classes would create much more complexity when handling the array in the view model and also make more complicated the List refreshing/identifying mechanism seems to me.
In the example the model does not need the #State, since changes are only coming from outside, in real world i would need it when it's the view itself to trigger the changes?
This is a stripped down version to reproduce the issue (create a project and replace ContentView with following):
import SwiftUI
struct Deck: Identifiable {
let id: Int
var name: String
init(_ name: String, _ id: Int) {
self.name = name
self.id = id
}
}
struct ItemView: View {
// #State var deck: Deck // DOES NOT WORK !!! <-------------------
let deck: Deck // WORKS (first element is updated)
var body: some View { Text(deck.name) }
}
class Model: ObservableObject {
#Published var decks: [Deck] = getData()
static func getData(changed: Bool = false) -> [Deck] {
let firstElement = changed ? "CHANGED ELEMENT" : "0"
return [Deck(firstElement, 0), Deck("1", 1), Deck("2", 2)]
}
func changeFirst() { self.decks = Self.getData(changed: true) }
}
struct ContentView: View {
#StateObject var model = Model()
var body: some View {
List {
ForEach(model.decks) { deck in
ItemView(deck: deck)
}
Button(action: model.changeFirst) {
Text("Change first item")
}
}
}
}
Tested with Xcode 13 / iPhone13 Simulator (iOS 15)
Question 1
Yes, it is expected because #State and #Published are sources of truth. #State breaks the connection with #Published and makes a copy.
Question 2
If all the changes are outside (one-way connection) you don't need wrappers of any kind for the children when dealing with value types.
If you need a two-way connection you use #Binding when dealing with a struct/value type.
https://developer.apple.com/wwdc21/10022
https://developer.apple.com/documentation/swiftui/managing-user-interface-state
https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app

Can't show view in Swift UI with layers of views and onAppear

There is a strange case where if you show a view through another view the contents (list of 3 items) of the second view won't show when values are set using onAppear. I'm guessing SwiftUI gets confused since the second views onAppear is called prior to the first views onAppear, but I still think this is weird since both of the views data are only used in their own views. Also, there is no problem if I don't use view models and instead have the data being set using state directly in the view, but then there is yet another problem that the view model declaration must be commented out otherwise I get "Thread 1: EXC_BAD_ACCESS (code=1, address=0x400000008)". Furthermore, if I check for nil in the first view on the data that is set there before showing the second one, then the second view will be shown the first time you navigate (to the first containing the second), but no other times. I also tried removing content view and starting directly at FirstView and then the screen is just black. I want to understand why these problems happen, setting data through init works but then the init will be called before it's navigated to since that's how NavigationView works, which in turn I guess I could work around by using a deferred view, but there are cases where I would like to do stuff in the background with .task as well and it has the same problem as .onAppear. In any case I would like to avoid work arounds and understand the problem. See comments for better explanation:
struct ContentView: View {
var body: some View {
NavigationView {
// If I directly go to SecondView instead the list shows
NavigationLink(destination: FirstView()) {
Text("Go to first view")
}
}
}
}
class FirstViewViewModel: ObservableObject {
#Published var listOfItems: [Int]?
func updateList() {
listOfItems = []
}
}
struct FirstView: View {
#ObservedObject var viewModel = FirstViewViewModel()
// If I have the state in the view instead of the view model there is no problem.
// Also need to comment out the view model when using the state otherwise I get Thread 1: EXC_BAD_ACCESS runtime exception
//#State private var listOfItems: [Int]?
var body: some View {
// Showing SecondView without check for nil and it will never show
SecondView()
// If I check for nil then the second view will show the first time its navigated to, but no other times.
/*Group {
if viewModel.listOfItems != nil {
SecondView()
} else {
Text("Loading").hidden() // Needed in order for onAppear to trigger, EmptyView don't work
}
}*/
// If I comment out onAppear there is no problem
.onAppear {
print("onAppear called for first view after onAppear in second view")
viewModel.updateList()
// listOfItems = []
}
}
}
class SecondViewViewModel: ObservableObject {
#Published var listOfItems = [String]()
func updateList() {
listOfItems = ["first", "second", "third"]
}
}
struct SecondView: View {
#ObservedObject var viewModel = SecondViewViewModel()
// If I set the items through init instead of onAppear the list shows every time
init() {
// viewModel.updateList()
}
var body: some View {
Group {
List {
ForEach(viewModel.listOfItems, id: \.self) { itemValue in
VStack(alignment: .leading, spacing: 8) {
Text(itemValue)
}
}
}
}
.navigationTitle("Second View")
.onAppear {
viewModel.updateList()
// The items are printed even though the view don't show
print("items: \(viewModel.listOfItems)")
}
}
}
We don't use view model objects in SwiftUI. For data transient to a View we use #State and #Binding to make the View data struct behave like an object.
And FYI initing an object using #ObservedObject is an error causing a memory leak, it will be discarded every time the View struct is init. When we are creating a Combine loader/fetcher object that we want to have a lifetime tied to the view we init the object using #StateObject.
Also you must not do id: \.self with ForEach for an array of value types cause it'll crash when the data changes. You have to make a struct for your data that conforms to Identifiable to be used with ForEach. Or if you really do want a static ForEach you can do ForEach(0..<5) {

SwiftUI 2 View Models in a View

I have this view which takes as a param its own View Model. Inside this view I need to show another view which in order for it to be displayed, needs its own View Model. So I present the Purchase view and then inside the Purchase view I need to display the details of the actual item which was purchased. So I pass as a parameter also the ItemViewModel. Both PurchaseViewModel and ItemViewModel are used on other views also.
It could happen in the feature that I would also need ClientViewModel which would be used to display data about the actual buyer of this item. Does this mean that I would need to pass also #ObservedObject var clientViewModel: ClientViewModel as a parameter?
My question is: Is this approach good or there is a better way to do this ?
struct PurchaseView: View {
#ObservedObject var purchaseViewModel: PurchaseViewModel
#ObservedObject var itemViewModel: ItemViewModel
var body: some View {
VStack {
Text(purchaseViewModel.title)
ItemView(itemViewModel: itemViewModel)
}
}
}
class Purchase {
let senderID: String
let receiverID: String
var status: PurchaseStatus
var item: Item?
}
You could make the PurchaseViewModel contain the ItemViewModel and ClientViewModel as properties in the same way that the PurchaseView contains the ItemView and ClientView.
That would make PurchaseViewModel responsible for creating the instances of the other view models. The parent of PurchaseView would no longer need to know about the implementation of PurchaseView.
You should start by considering the relationship of the models, not the view models.
For example, you could make a case that a Purchase involves one or more items (Item) and a purchaser (Client). In this case you would actually create a model something like:
class Purchase {
var items = [Item]()
var client: Client
...
}
And your PurchaseViewModel would reflect this, containing the ItemViewModel and ClientViewModel
let clientViewModel: ClientViewModel
let itemsViewModels: [ItemViewModel]
init(purchase: Purchase) {
self.clientViewModel = ClientViewModel(client:purchase.client)
self.itemsViewModels = purchase.items.map { ItemViewModel(item:$0) }
}
If you have a hierarchy of views that need some unrelated models then you can consider using the environment, especially for models that are "global"
For example, say that the user was looking at an item detail, but they were not yet making a purchase (So there is no Purchase yet) and you want to have a button that shows them their Client detail.
You could pass the ItemViewModel explicitly to the ItemDetailView:
struct ItemListView {
...
Button(action: { ItemDetailView(item:someItemView) })
{ ... }
}
struct ItemDetailView {
...
Button(action: { ClientView() }) {
Text("Client detail")
}
}
struct ClientDetailView {
#EnvironmentObject client: ClientViewModel
...
}
You can inject the ClientViewModel into the environment at a suitable point, such as the root view or even scene delegate.
If you have a hierarchy of views that need these models, but not all views need all models, you could use the environment to provide the various models rather than directly injecting them via initialiser parameters.

iOS SwiftUI: ObservableObject from parent to child

Problem:
Passing down the view model and modifying it from the children, won't refresh the parent.
What I want:
Whenever I modify the view model from the child view, the parent refresh too.
Problem description:
I'm looking for a way to pass down a ViewModel ObservedObject to some child views.
This is my struct:
struct Parent: View {
#ObservedObject viewModel: ViewModel
var body: some View {
NavigationView {
Form {
ScrollView(showsIndicators: false) {
Section {
ChildViewOne(model: viewModel)
}
Section {
ChildViewTwo(model: viewModel)
}
Section {
ChildViewThree(model: viewModel)
}
}
}
}
}
}
struct AnyChild: View {
#ObservedObject viewModel: ViewModel
var body: some View {
// some stuff
}
}
In this way, whenever I modify from the child views the view model, the children rebuild correctly, but the parent won't, and the scrollview does not resize correctly.
I would like to pass down to the children the viewModel like a Binding object, so the parent will refresh too.
But I don't know how to do. Any help?
Solution:
I had a specific problem with the Form when I've removed everything worked fine.
But still the correct solution as #Alladinian wrote is to use the #EnvironmentObject
Instead of passing the same model to each subview, you can use #EnvironmentObject instead which is specifically designed for this.
You essentially have to inject your ObservableObject in your parent view with something like this (typically in your appdelegate before presenting the parent view, or in your previews setup for live previewing):
Parent().environmentObject(ViewModel())
then you can automatically get a reference in each view with something like this:
struct AnyChild: View {
#EnvironmentObject var model: ViewModel
//...
}
and use it with your bindings
Here is a brief article about the use of environment
Finally, regarding why your solution is not working, you must consider a single source of truth (the observable object in your Parent) and a #Binding for each subview that will eventually change the state of this object. I hope that this makes sense...

Resources