SwiftUI: ParentView not updating when deleting CoreData item of child view using MVVM - swift

I am using MVVM in combination with CoreData to separate all logic and data management from my views.
I have a scrollable list view that contains all items of an object stored in Core Data. It consists of a parent view (scrollable view) with an ForEach to create the child views (items)
The child view should have the functionality to delete the item.
When implementing this, I can see that the contents are deleted, but in my parent view, I still see the item with empty contents, until I close and reopen the app.
Here is the rough structure of my code:
// objectViewModel
class ObjectViewModel: ObservableObject {
#published subobjects: [SubobjectViewModel]
}
// subobjectViewModel
class SubobjectViewModel: Observable, Identifiable {
#published subobject
func delete() {
// delete from CoreData
viewContext.delete(self.subobject)
objectWillChange.send()
}
}
// parentView
#StateObject var object: ObjectViewModel
...
ScrollView {
ForEach (object.subobjects) { subobject in
SubobjectView(subobject)
}
}
...
// childView
#StateObject var subobject: SubobjectViewModel
...
Button(action: {
self.subobject.delete()
}) {
Text("delete")
}
...
Expected behaviour: When clicking on the delete button in the child view, the item gets delete in the core data and should be removed from the subobjects-array in the ObjectViewModel, to update the parent view.

Related

SwiftUI: How do I share an ObservedObject between child views without affecting the parent view

TL;DR: If I have a view containing a NavigationSplitView(sidebar:detail:), with a property (such as a State or StateObject) tracking user selection, how should I make it so that the sidebar and detail views observe the user selection, but the parent view does not?
Using SwiftUI's new NavigationSplitView (or the deprecated NavigationView), a common paradigm is to have a list of selectable items in the sidebar view, with details of the selected item in the detail view. The selection, of course, needs to be observed, usually from within an ObservedObject.
struct ExampleView: View {
#StateObject private var viewModel = ExampleViewModel()
var body: some View {
NavigationSplitView {
SidebarView(selection: $viewModel.selection)
} detail: {
DetailView(item: viewModel.selection)
}
}
}
struct SidebarView: View {
let selectableItems: [Item] = []
#Binding var selection: Item?
var body: some View {
List(selectableItems, selection: $viewModel.selected) { item in
NavigationLink(value: item) { Text(item.name) }
}
}
}
struct DetailView: View {
let item: Item?
var body: some View {
// Details of the selected item
}
}
#MainActor
final class ExampleViewModel: ObservableObject {
#Published var selection: Item? = nil
}
This, however, poses a problem: the ExampleView owns the ExampleViewModel used for tracking the selection, which means that it gets recalculated whenever the selection changes. This, in turn, causes its children SidebarView and DetailView to be redrawn.
Since we want those children to be recalculated, one might be forgiven for thinking that everything is as intended. However, the ExampleView itself should not be recalculated in my opinion, because doing so will not only update the child views (intended), but also everything in the parent view (not intended). This is especially true if its body is composed of other views, modifiers, or setup work. Case in point: in this example, the NavigationSplitView itself will also be recalculated, which I don't think is what we want.
Almost all tutorials, guides and examples I see online use a version of the above example - sometimes the viewModel is passed as an ObservedObject, or as an EnvironmentObject, but they all share the same trait in that the parent view containing the NavigationSplitView is observing the property that should only be observed by the children of NavigationSplitView.
My current solution is to initiate the viewmodel in the parent view, but not observe it:
struct ExampleView: View {
let viewModel = ExampleViewModel()
...
}
#MainActor
final class ExampleViewModel: ObservableObject {
#Published var selection: Item? = nil
nonisolated init() { }
}
This way, the parent view will remain intact (at least in regards to user selection); however, this will cause the ExampleViewModel to be recreated if anything else would cause the ExampleView to be redrawn - effectively resetting our user selection. Additionally, we are unable to pass any of the viewModel's properties as bindings. So while it works for my current use-case, I don't consider this an effective solution.

SwiftUI & MVVM: How to animate "list elements change" when view model changes the "data source" (`#Publish items`) itself

I am still relatively new to SwfitUI and Combine so maybe I am trying to do something very incorrectly but I just cannot see how to achieve what I am aiming to do with SwiftUI and MVVM.
Here is the scenario:
ViewModel class with a property #Published var items = [String]()
Main view (HomeView) that has a ForEach showing the items from its view model
HomeView has a #StateObject var viewModel: ViewModel
The HomeView ForEach uses the items from viewModel
ViewModel changes the items (in my case core data changes)
The HomeView ForEach reflects the change immediately
This all works, but what I want to do is animate the elements in the ForEach changing due to the viewModel.items changing.
What I can do is import SwiftUI into the ViewModel and use withAnimation to wrap the setting of new items. But this beats the purpose of the ViewModel as it now has a direct reference to UI code.
Here some code I have:
struct HomeView: View {
#StateObject var viewModel: ViewModel
var body: some View {
ForEach(items) { item in
Text(item)
}
}
}
import SwiftUI // This should not be imported as it breaks MVVM patter
class ViewModel {
#Published var items = [String]()
func onItemsChanged(_ newItems: [String]) {
withAnimation { // This works but will break MVVM patter
items = newItems
}
}
}
Any ideas if this can be achieve to make MVVM happy and work with SwiftUI?
Add animation to the container which hold your view items, like below
var body: some View {
VStack { // << container
ForEach(items) { item in
Text(item)
}
}
.animation(.default) // << animates changes in items
}
See next posts for complete examples: https://stackoverflow.com/a/60893462/12299030, https://stackoverflow.com/a/65776506/12299030, https://stackoverflow.com/a/63364795/12299030.

How to reset a subview in SwiftUI?

Below is a simplified version of the code that I'm using. But whenever I resetKeyboard() it still shows the previous keyboard. Is there anyway to make it so when I call resetKeyboard() it replaces the keyboard with a fresh KeyboardView?
struct GameView: View {
#State var myKeyboard = KeyboardView()
var body: some View {
VStack{
Button("Change Keyboard") {
myKeyboard.change()
}
myKeyboard
Button("Reset Keyboard") {
resetKeyboard()
}
}
}
func resetKeyboard(){
self.myKeyboard = KeyboardView()
}
}
SwiftUI constructs a view tree from View objects in body of their parents.
So, what SwiftUI got was the initial copy (remember, it's a value-type struct) of myKeyboard, not the copy you are changing.
Under normal usage, you don't keep instances of various View stored as variables (I mean, you can, but you'd need to understand in depth what's going on).
What you probably want is to change the data that drives the child view. Where does (should) this data live? It depends on what you want to do.
In most cases the parent "owns" the state, i.e. has the source of truth of some data that the child relies on. Then it's trivial to change the state and pass the state to the child via its init:
struct ChildView: View {
let number: Int
var body: some View {
Text("\(number)")
}
}
struct ParentView: View {
#State var random: Int = Int.random(1...100)
var body: some View {
VStack() {
ChildView(number: random)
Button("randomize") {
self.random = Int.random(1...100)
}
}
}
}
But, say, the parent doesn't want to do the randomization - i.e. the child should deal with it.
The proper approach is to create a view model for the child, which the parent (or the parent's own view model) could own and pass via init, and then the view model would deal with nuances of randomization.
class ChildVM: ObservableObject {
#Published var random = Int.random(1...100)
func change() {
random = Int.random(1...100)
}
}
The parent creates an instance of ChildVM and passes it to the child:
struct ParentVuew: View {
let childVM = ChildVM()
var body: some View {
VStack() {
ChildView(vm: childVm)
Button("randomize") {
self.childVM.change()
}
}
}
}
And the child view is simply driven by the view model:
struct ChildView: View {
#ObservedObject let vm: ChildVM
var body: some View {
Text("\(vm.random)")
}
}
Obviously, this is a simplified example that could have been achieved in any number of ways.
And there are different ways for the parent to "message" the child.
But the general takeaway should be that Views should be thought of as declarative structures - not living instances - and the data is what drives the changes in those views. You need to decide who is best to own the source of truth.

SWIFTUI - OberservableObject class being set to nill each time called

I am trying to learn Swift, and swiftUI.
I have a class : ObservableObject called within a button.
the class fetches some data, and stores it in a data var (an array of (string:any))
the class function fetch data is called within onAppear function in the main view VStack.
the results of the function populates a list.
it all works fine, but each time I recall the view, the list empties itself.
here s the class opening
class fetchDataModel: ObservableObject {
#Published var datas:[data] = [data]()
now the datalist view :
struct allData: View {
#ObservedObject var datas = fetchDataModel()
var body: some View {
VStack{
NavigationView {
List(datas.data){ data in
NavigationLink(destination: data(data: data)){
HStack{
Text(data.someText)
}
}
}
}
}.navigationBarTitle(Text("Landmarks"))
.onAppear(){
self.datas.fetchData()
}
}
}
I have a side menu that populates a motherView, once a button from the side menu is clicked, it calls datalist() (session is an environmentalObject)
if session.currentPage == "data" {
myview = AnyView(allDatas())
}
So I guess, each time I set myView to allDatas, it reinitialises the datas = fetchDataModel() class, and sets all its values to 0. Is there a way to avoid this problem ?

ObervableObject being init multiple time, and not refreshing my view

I have a structure like this:
contentView {
navigationView {
foreach {
NavigationLink(ViewA(id: id))
}
}
}
/// where ViewA contains an request trigger when it appears
struct ViewA: View {
#State var filterString: String = ""
var id: String!
#ObservedObject var model: ListObj = ListObj()
init(id: String) {
self.id = id
}
var body: some View {
VStack {
SearchBarView(searchText: $filterString)
List {
ForEach(model.items.filter({ filterString.isEmpty || $0.id.contains(filterString) || $0.name.contains(filterString) }), id: \.id) { item in
NavigationLink(destination: ViewB(id: item.id)) {
VStack {
Text("\(item.name) ")
}
}
}
}
}
.onAppear {
self.model.getListObj(id: self.id) //api request, fill data and call objectWillChange.send()
}
}
}
ViewB has the same code as ViewA: It receives an id, stores and requests an API to collect data.
But the viewB list is not being refreshed.
I also noticed that viewB's model property
#ObservedObject var model: model = model()
was instantiated multiple times.
Debugging, I found that every navigationLink instantiates its destination even before it is triggered. That's not a problem usually, but in my case i feel like the ViewB model is being instantiated 2 times, and my onAppear call the wrong one, reason why self.objectWillChange.send() not refreshing my view.
There are two issues here:
SwiftUI uses value types that that get initialized over and over again each pass through body.
Related to #1, NavigationLink is not lazy.
#1
A new ListObj gets instantiated every time you call ViewA.init(...). ObservedObject does not work the same as #State where SwiftUI keeps careful track of it for you throughout the onscreen lifecycle. SwiftUI assumes that ultimate ownership of an #ObservedObject exists at some level above the View it's used in.
In other words, you should almost always avoid things like #ObservedObject var myObject = MyObservableObject().
(Note, even if you did #State var model = ListObj() it would be instantiated every time. But because it's #State SwiftUI will replace the new instance with the original before body gets called.)
#2
In addition to this, NavigationLink is not lazy. Each time you instantiate that NavigationLink you pass a newly instantiated ViewA, which instantiates your ListObj.
So for starters, one thing you can do is make a LazyView to delay instantiation until NavigationLink.destination.body actually gets called:
// Use this to delay instantiation when using `NavigationLink`, etc...
struct LazyView<Content: View>: View {
var content: () -> Content
var body: some View {
self.content()
}
}
Now you can do NavigationLink(destination: LazyView { ViewA() }) and instantiation of ViewA will be deferred until the destination is actually shown.
Simply using LazyView will fix your current problem as long as it's the top view in the hierarchy, like it is when you push it in a NavigationView or if you present it.
However, this is where #user3441734's comment comes in. What you really need to do is keep ownership of model somewhere outside of your View because of what was explained in #1.
If your #ObservedObject is being initialized multiple times, it is because the owner of the object is refreshed and recreated every time it has state changes. Try to use #StateObject if your app is iOS 14 and above. It prevents the object from being recreated when the view refreshes.
https://developer.apple.com/documentation/swiftui/stateobject
When a view creates its own #ObservedObject instance it is recreated
every time a view is discarded and redrawn. On the contrary a #State
variable will keep its value when a view is redrawn. A #StateObject is
a combination of #ObservedObject and #State - the instance of the
ViewModel will be kept and reused even after a view is discarded and
redrawn
What is the difference between ObservedObject and StateObject in SwiftUI