Create an account

Very important

  • To access the important data of the forums, you must be active in each forum and especially in the leaks and database leaks section, send data and after sending the data and activity, data and important content will be opened and visible for you.
  • You will only see chat messages from people who are at or below your level.
  • More than 500,000 database leaks and millions of account leaks are waiting for you, so access and view with more activity.
  • Many important data are inactive and inaccessible for you, so open them with activity. (This will be done automatically)


Thread Rating:
  • 472 Vote(s) - 3.48 Average
  • 1
  • 2
  • 3
  • 4
  • 5
SwiftUI: ObservableObject does not persist its State over being redrawn

#1
# Problem

In Order to achieve a clean look and feel of the App's code, I create ViewModels for every View that contains logic.

A normal ViewModel looks a bit like this:

```
class SomeViewModel: ObservableObject {

@Published var state = 1

// Logic and calls of Business Logic goes here
}
```

and is used like so:

```
struct SomeView: View {

@ObservedObject var viewModel = SomeViewModel()

var body: some View {
// Code to read and write the State goes here
}
}
```

This workes fine when the Views Parent is not being updated. If the parent's state changes, this View gets redrawn (pretty normal in a declarative Framework). **But** also the ViewModel gets recreated and does not hold the State afterward. This is unusual when you compare to other Frameworks (eg: Flutter).

*In my opinion, the ViewModel should stay, or the State should persist.*

If I replace the ViewModel with a `@State` Property and use the `int` (in this example) directly it stays persisted and **does not get recreated**:

```
struct SomeView: View {

@State var state = 1

var body: some View {
// Code to read and write the State goes here
}
}
```

This does obviously not work for more complex States. And if I set a class for `@State` (like the ViewModel) more and more Things are not working as expected.

# Question

- Is there a way of not recreating the ViewModel every time?
- Is there a way of replicating the `@State` Propertywrapper for `@ObservedObject`?
- Why is @State keeping the State over the redraw?

I know that usually, it is bad practice to create a ViewModel in an inner View but this behavior can be replicated by using a NavigationLink or Sheet.<br>
Sometimes it is then just not useful to keep the State in the ParentsViewModel and work with bindings when you think of a very complex TableView, where the Cells themself contain a lot of logic.<br>
There is always a workaround for individual cases, but I think it would be way easier if the ViewModel would not be recreated.

# Duplicate Question

I know there are a lot of questions out there talking about this issue, all talking about very specific use-cases. Here I want to talk about the general problem, without going too deep into custom solutions.

# Edit (adding more detailed Example)

When having a State-changing ParentView, like a list coming from a Database, API, or cache (think about something simple). Via a `NavigationLink` you might reach a Detail-Page where you can modify the Data. By changing the data the reactive/declarative Pattern would tell us to also update the ListView, which would then "redraw" the `NavigationLink`, which would then lead to a recreation of the ViewModel.

I know I could store the ViewModel in the ParentView / ParentView's ViewModel, but this is the wrong way of doing it IMO. And since subscriptions are destroyed and/or recreated - there might be some side effects.
Reply

#2
My solution is use EnvironmentObject and don't use ObservedObject at view it's viewModel will be reset, you pass through hierarchy by
```
.environmentObject(viewModel)
```
Just init viewModel somewhere it will not be reset(example root view).
Reply

#3
Finally, there is a Solution provided by Apple: `@StateObject`.

By replacing `@ObservedObject` with `@StateObject` everything mentioned in my initial post is working.

Unfortunately, this is only available in ios 14+.

This is my Code from Xcode 12 Beta (Published June 23, 2020)

```
struct ContentView: View {

@State var title = 0

var body: some View {
NavigationView {
VStack {
Button("Test") {
self.title = Int.random(in: 0...1000)
}

TestView1()

TestView2()
}
.navigationTitle("\(self.title)")
}
}
}

struct TestView1: View {

@ObservedObject var model = ViewModel()

var body: some View {
VStack {
Button("Test1: \(self.model.title)") {
self.model.title += 1
}
}
}
}

class ViewModel: ObservableObject {

@Published var title = 0
}

struct TestView2: View {

@StateObject var model = ViewModel()

var body: some View {
VStack {
Button("StateObject: \(self.model.title)") {
self.model.title += 1
}
}
}
}
```

As you can see, the `StateObject` Keeps it value upon the redraw of the Parent View, while the `ObservedObject` is being reset.
Reply

#4
I agree with you, I think this is one of many major problems with SwiftUI. Here's what I find myself doing, as gross as it is.

```
struct MyView: View {
@State var viewModel = MyViewModel()

var body : some View {
MyViewImpl(viewModel: viewModel)
}
}

fileprivate MyViewImpl : View {
@ObservedObject var viewModel : MyViewModel

var body : some View {
...
}
}
```

You can either construct the view model in place or pass it in, and it gets you a view that will maintain your ObservableObject across reconstruction.

Reply

#5
You need to provide custom `PassThroughSubject` in your `ObservableObject` class. Look at this code:

//
// Created by Франчук Андрей on 08.05.2020.
// Copyright © 2020 Франчук Андрей. All rights reserved.
//

import SwiftUI
import Combine


struct TextChanger{
var textChanged = PassthroughSubject<String,Never>()
public func changeText(newValue: String){
textChanged.send(newValue)
}
}

class ComplexState: ObservableObject{
var objectWillChange = ObservableObjectPublisher()
let textChangeListener = TextChanger()
var text: String = ""
{
willSet{
objectWillChange.send()
self.textChangeListener.changeText(newValue: newValue)
}
}
}

struct CustomState: View {
@State private var text: String = ""
let textChangeListener: TextChanger
init(textChangeListener: TextChanger){
self.textChangeListener = textChangeListener
print("did init")
}
var body: some View {
Text(text)
.onReceive(textChangeListener.textChanged){newValue in
self.text = newValue
}
}
}
struct CustomStateContainer: View {
//@ObservedObject var state = ComplexState()
var state = ComplexState()
var body: some View {
VStack{
HStack{
Text("custom state View: ")
CustomState(textChangeListener: state.textChangeListener)
}
HStack{
Text("ordinary Text View: ")
Text(state.text)
}
HStack{
Text("text input: ")
TextInput().environmentObject(state)
}
}
}
}

struct TextInput: View {
@EnvironmentObject var state: ComplexState
var body: some View {
TextField("input", text: $state.text)
}
}

struct CustomState_Previews: PreviewProvider {
static var previews: some View {
return CustomStateContainer()
}
}

First, I using `TextChanger` to pass new value of `.text` to `.onReceive(...)` in `CustomState` View. Note, that `onReceive` in this case gets `PassthroughSubject`, not the `ObservableObjectPublisher`. In last case you will have only `Publisher.Output` in `perform: closure`, not the NewValue. `state.text` in that case would have old value.

Second, look at the `ComplexState` class. I made an `objectWillChange` property to make text changes send notification to subscribers manually. Its almost the same like `@Published` wrapper do. But, when the text changing it will send both, and `objectWillChange.send()` and `textChanged.send(newValue)`. This makes you be able to choose in exact `View`, how to react on state changing. If you want ordinary behavior, just put the state into `@ObservedObject` wrapper in `CustomStateContainer` View. Then, you will have all the views recreated and this section will get updated values too:

HStack{
Text("ordinary Text View: ")
Text(state.text)
}
If you don't want all of them to be recreated, just remove @ObservedObject. Ordinary text View will stop updating, but CustomState will. With no recreating.

update:
If you want more control, you can decide while changing the value, who do you want to inform about that change.
Check more complex code:

//
//
// Created by Франчук Андрей on 08.05.2020.
// Copyright © 2020 Франчук Андрей. All rights reserved.
//

import SwiftUI
import Combine


struct TextChanger{
// var objectWillChange: ObservableObjectPublisher
// @Published
var textChanged = PassthroughSubject<String,Never>()
public func changeText(newValue: String){
textChanged.send(newValue)
}
}

class ComplexState: ObservableObject{
var onlyPassthroughSend = false
var objectWillChange = ObservableObjectPublisher()
let textChangeListener = TextChanger()
var text: String = ""
{
willSet{
if !onlyPassthroughSend{
objectWillChange.send()
}
self.textChangeListener.changeText(newValue: newValue)
}
}
}

struct CustomState: View {
@State private var text: String = ""
let textChangeListener: TextChanger
init(textChangeListener: TextChanger){
self.textChangeListener = textChangeListener
print("did init")
}
var body: some View {
Text(text)
.onReceive(textChangeListener.textChanged){newValue in
self.text = newValue
}
}
}
struct CustomStateContainer: View {
//var state = ComplexState()
@ObservedObject var state = ComplexState()
var body: some View {
VStack{
HStack{
Text("custom state View: ")
CustomState(textChangeListener: state.textChangeListener)
}
HStack{
Text("ordinary Text View: ")
Text(state.text)
}
HStack{
Text("text input with full state update: ")
TextInput().environmentObject(state)
}
HStack{
Text("text input with no full state update: ")
TextInputNoUpdate().environmentObject(state)
}
}
}
}

struct TextInputNoUpdate: View {
@EnvironmentObject var state: ComplexState
var body: some View {
TextField("input", text: Binding( get: {self.state.text},
set: {newValue in
self.state.onlyPassthroughSend.toggle()
self.state.text = newValue
self.state.onlyPassthroughSend.toggle()
}
))
}
}

struct TextInput: View {
@State private var text: String = ""
@EnvironmentObject var state: ComplexState
var body: some View {

TextField("input", text: Binding(
get: {self.text},
set: {newValue in
self.state.text = newValue
// self.text = newValue
}
))
.onAppear(){
self.text = self.state.text
}.onReceive(state.textChangeListener.textChanged){newValue in
self.text = newValue
}
}
}

struct CustomState_Previews: PreviewProvider {
static var previews: some View {
return CustomStateContainer()
}
}

I made a manual Binding to stop broadcasting objectWillChange. But you still need to gets new value in all the places you changing this value to stay synchronized. Thats why I modified TextInput too.

Is that what you needed?

Reply

#6
> Is there a way of not recreating the ViewModel every time?

Yes, keep ViewModel instance **outside** of `SomeView` and inject via constructor

```
struct SomeView: View {
@ObservedObject var viewModel: SomeViewModel // << only declaration

```

> Is there a way of replicating the @State Propertywrapper for @ObservedObject?

No needs. `@ObservedObject` is-a already `DynamicProperty` similarly to `@State`

> Why is @State keeping the State over the redraw?

Because it keeps its storage, ie. wrapped value, **outside** of view. (so, see first above again)
Reply



Forum Jump:


Users browsing this thread:
1 Guest(s)

©0Day  2016 - 2023 | All Rights Reserved.  Made with    for the community. Connected through